Skip to content

Commit 5ffb9aa

Browse files
Alex HolmbergAlex Holmberg
authored andcommitted
feat(21-01): add POST /message endpoint
- Add post_message handler accepting RunAgentInput JSON body - Route messages to agent processor via message channel - Return JSON acknowledgment with thread_id and run_id - Add route at /message in server Router - Add test for POST message acceptance and routing
1 parent 2d711b1 commit 5ffb9aa

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

src/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ impl AgUiServer {
213213
.route("/", get(routes::health))
214214
.route("/sse", get(routes::sse_handler))
215215
.route("/ws", get(routes::ws_handler))
216+
.route("/message", post(routes::post_message))
216217
.route("/health", get(routes::health))
217218
.with_state(self.state);
218219

src/server/routes.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,43 @@ pub async fn health() -> Json<serde_json::Value> {
3434
}))
3535
}
3636

37+
/// POST endpoint for receiving messages via HTTP.
38+
///
39+
/// Alternative to WebSocket for clients that prefer REST.
40+
/// Accepts RunAgentInput JSON body and routes to agent processor.
41+
pub async fn post_message(
42+
State(state): State<ServerState>,
43+
Json(input): Json<RunAgentInput>,
44+
) -> Json<serde_json::Value> {
45+
let thread_id = input.thread_id.to_string();
46+
let run_id = input.run_id.to_string();
47+
48+
debug!(
49+
thread_id = %thread_id,
50+
run_id = %run_id,
51+
message_count = input.messages.len(),
52+
"Received RunAgentInput via POST"
53+
);
54+
55+
let message_tx = state.message_sender();
56+
let agent_msg = AgentMessage::new(input);
57+
58+
match message_tx.send(agent_msg).await {
59+
Ok(_) => Json(json!({
60+
"status": "accepted",
61+
"thread_id": thread_id,
62+
"run_id": run_id
63+
})),
64+
Err(e) => {
65+
warn!("Failed to route message to agent processor: {}", e);
66+
Json(json!({
67+
"status": "error",
68+
"message": "Failed to route message to agent processor"
69+
}))
70+
}
71+
}
72+
}
73+
3774
/// SSE endpoint for streaming AG-UI events.
3875
pub async fn sse_handler(
3976
State(state): State<ServerState>,
@@ -138,11 +175,40 @@ async fn handle_websocket(socket: WebSocket, state: ServerState) {
138175
#[cfg(test)]
139176
mod tests {
140177
use super::*;
178+
use ag_ui_core::types::Message as AgUiProtocolMessage;
179+
use ag_ui_core::{RunId, ThreadId};
180+
use axum::extract::State;
141181

142182
#[tokio::test]
143183
async fn test_health_endpoint() {
144184
let response = health().await;
145185
assert_eq!(response.0["status"], "ok");
146186
assert_eq!(response.0["protocol"], "ag-ui");
147187
}
188+
189+
#[tokio::test]
190+
async fn test_post_message_accepted() {
191+
use crate::server::ServerState;
192+
193+
let state = ServerState::new();
194+
let mut msg_rx = state.take_message_receiver().await.expect("Should get receiver");
195+
196+
// Create RunAgentInput
197+
let thread_id = ThreadId::random();
198+
let run_id = RunId::random();
199+
let input = RunAgentInput::new(thread_id.clone(), run_id.clone())
200+
.with_messages(vec![AgUiProtocolMessage::new_user("Hello from POST")]);
201+
202+
// Call post_message handler
203+
let response = post_message(State(state), Json(input)).await;
204+
205+
// Verify response
206+
assert_eq!(response.0["status"], "accepted");
207+
assert_eq!(response.0["thread_id"], thread_id.to_string());
208+
assert_eq!(response.0["run_id"], run_id.to_string());
209+
210+
// Verify message was routed
211+
let received = msg_rx.recv().await.expect("Should receive message");
212+
assert_eq!(received.input.messages.len(), 1);
213+
}
148214
}

0 commit comments

Comments
 (0)