Skip to content

Commit 4c54316

Browse files
committed
feat(goose-acp): new request patterns per ACP RFD 721
1 parent 67b9020 commit 4c54316

3 files changed

Lines changed: 439 additions & 186 deletions

File tree

crates/goose-acp/src/transport.rs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,19 @@ use axum::{
1515
Router,
1616
};
1717
use serde_json::Value;
18-
use tokio::sync::{mpsc, Mutex};
1918
use tower_http::cors::{Any, CorsLayer};
2019

2120
use crate::server_factory::AcpServer;
2221

22+
pub(crate) const HEADER_CONNECTION_ID: &str = "Acp-Connection-Id";
2323
pub(crate) const HEADER_SESSION_ID: &str = "Acp-Session-Id";
2424
pub(crate) const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
2525
pub(crate) const JSON_MIME_TYPE: &str = "application/json";
2626

27-
pub(crate) struct TransportSession {
28-
pub to_agent_tx: mpsc::Sender<String>,
29-
pub from_agent_rx: Arc<Mutex<mpsc::UnboundedReceiver<String>>>,
30-
pub handle: tokio::task::JoinHandle<()>,
31-
}
32-
33-
pub(crate) fn accepts_mime_type(request: &Request<Body>, mime_type: &str) -> bool {
34-
request
35-
.headers()
36-
.get(axum::http::header::ACCEPT)
37-
.and_then(|v| v.to_str().ok())
38-
.is_some_and(|accept| accept.contains(mime_type))
39-
}
40-
4127
pub(crate) fn accepts_json_and_sse(request: &Request<Body>) -> bool {
4228
request
4329
.headers()
44-
.get(axum::http::header::ACCEPT)
30+
.get(header::ACCEPT)
4531
.and_then(|v| v.to_str().ok())
4632
.is_some_and(|accept| {
4733
accept.contains(JSON_MIME_TYPE) && accept.contains(EVENT_STREAM_MIME_TYPE)
@@ -51,11 +37,19 @@ pub(crate) fn accepts_json_and_sse(request: &Request<Body>) -> bool {
5137
pub(crate) fn content_type_is_json(request: &Request<Body>) -> bool {
5238
request
5339
.headers()
54-
.get(axum::http::header::CONTENT_TYPE)
40+
.get(header::CONTENT_TYPE)
5541
.and_then(|v| v.to_str().ok())
5642
.is_some_and(|ct| ct.starts_with(JSON_MIME_TYPE))
5743
}
5844

45+
pub(crate) fn get_connection_id(request: &Request<Body>) -> Option<String> {
46+
request
47+
.headers()
48+
.get(HEADER_CONNECTION_ID)
49+
.and_then(|v| v.to_str().ok())
50+
.map(|s| s.to_string())
51+
}
52+
5953
pub(crate) fn get_session_id(request: &Request<Body>) -> Option<String> {
6054
request
6155
.headers()
@@ -64,20 +58,43 @@ pub(crate) fn get_session_id(request: &Request<Body>) -> Option<String> {
6458
.map(|s| s.to_string())
6559
}
6660

61+
pub(crate) fn is_initialize_request(value: &Value) -> bool {
62+
value.get("method").is_some_and(|m| m == "initialize") && value.get("id").is_some()
63+
}
64+
65+
pub(crate) fn is_session_creating_request(value: &Value) -> bool {
66+
value
67+
.get("method")
68+
.and_then(|m| m.as_str())
69+
.is_some_and(|m| m == "session/new" || m == "session/load" || m == "session/fork")
70+
}
71+
6772
pub(crate) fn is_jsonrpc_request(value: &Value) -> bool {
6873
value.get("method").is_some() && value.get("id").is_some()
6974
}
7075

71-
pub(crate) fn is_jsonrpc_notification(value: &Value) -> bool {
72-
value.get("method").is_some() && value.get("id").is_none()
76+
pub(crate) fn is_jsonrpc_response_or_error(value: &Value) -> bool {
77+
value.get("id").is_some() && (value.get("result").is_some() || value.get("error").is_some())
7378
}
7479

75-
pub(crate) fn is_jsonrpc_response(value: &Value) -> bool {
76-
value.get("id").is_some() && (value.get("result").is_some() || value.get("error").is_some())
80+
/// Extract the JSON-RPC `id` from a message. Returns the id as a string
81+
/// regardless of whether it was originally a number or string.
82+
pub(crate) fn extract_jsonrpc_id(value: &Value) -> Option<String> {
83+
value.get("id").map(|id| match id {
84+
Value::String(s) => s.clone(),
85+
Value::Number(n) => n.to_string(),
86+
other => other.to_string(),
87+
})
7788
}
7889

79-
pub(crate) fn is_initialize_request(value: &Value) -> bool {
80-
value.get("method").is_some_and(|m| m == "initialize") && value.get("id").is_some()
90+
/// Extract `sessionId` from a JSON-RPC result body.
91+
/// Used by the transport to set `Acp-Session-Id` on session/new and session/load responses.
92+
pub(crate) fn extract_session_id_from_result(value: &Value) -> Option<String> {
93+
value
94+
.get("result")
95+
.and_then(|r| r.get("sessionId"))
96+
.and_then(|s| s.as_str())
97+
.map(|s| s.to_string())
8198
}
8299

83100
async fn handle_get(
@@ -99,13 +116,17 @@ pub fn create_router(server: Arc<AcpServer>) -> Router {
99116
let http_state = Arc::new(http::HttpState::new(server.clone()));
100117
let ws_state = Arc::new(websocket::WsState::new(server));
101118

119+
let connection_id_header = HEADER_CONNECTION_ID.parse().unwrap();
120+
let session_id_header = HEADER_SESSION_ID.parse().unwrap();
121+
102122
let cors = CorsLayer::new()
103123
.allow_origin(Any)
104124
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
105125
.allow_headers([
106126
header::CONTENT_TYPE,
107127
header::ACCEPT,
108-
HEADER_SESSION_ID.parse().unwrap(),
128+
connection_id_header,
129+
session_id_header,
109130
header::SEC_WEBSOCKET_VERSION,
110131
header::SEC_WEBSOCKET_KEY,
111132
header::CONNECTION,

0 commit comments

Comments
 (0)