use std::collections::HashMap;
use murmur_artifact::TaskAcceptance;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::oneshot;
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct JsonRpcRequest {
#[allow(dead_code)] pub jsonrpc: String,
pub id: Value,
pub method: String,
pub params: Value,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct JsonRpcResponse {
pub jsonrpc: &'static str,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
impl JsonRpcResponse {
pub(crate) fn ok(id: Value, result: impl Serialize) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
error: None,
}
}
pub(crate) fn err(id: Value, code: i32, message: &str) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(JsonRpcError {
code,
message: message.to_string(),
}),
}
}
pub(crate) fn into_http_response(self) -> String {
let body = serde_json::to_string(&self).unwrap_or_else(|_| "{}".to_string());
format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(),
body,
)
}
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct JsonRpcError {
pub code: i32,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct A2aMessage {
pub message_id: String,
pub context_id: Option<String>,
#[allow(dead_code)] pub role: String,
pub parts: Vec<MessagePart>,
}
impl A2aMessage {
pub(crate) fn extract_text(&self) -> String {
self.parts
.iter()
.filter_map(|p| p.text.as_deref())
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct MessagePart {
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct A2aTask {
pub id: String,
pub context_id: String,
pub status: TaskStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifacts: Option<Vec<A2aArtifact>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct A2aArtifact {
pub name: String,
pub parts: Vec<ArtifactPart>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ArtifactPart {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TaskStatus {
pub state: TaskState,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum TaskState {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Rejected,
}
#[derive(Debug, Clone)]
pub(crate) enum TaskSlotState {
Empty,
Running { task_id: String, context_id: String },
Done { task_id: String },
}
pub(crate) struct TaskRegistry {
pub(crate) active_slot: TaskSlotState,
pub(crate) history: HashMap<String, (TaskState, String)>,
pub(crate) pending_count: usize,
pub(crate) queue_depth: usize,
pub(crate) task_acceptance: TaskAcceptance,
input_waiters: HashMap<String, (String, oneshot::Sender<String>)>,
}
impl TaskRegistry {
pub(crate) fn new(queue_depth: usize, task_acceptance: TaskAcceptance) -> Self {
Self {
active_slot: TaskSlotState::Empty,
history: HashMap::new(),
pending_count: 0,
queue_depth,
task_acceptance,
input_waiters: HashMap::new(),
}
}
pub(crate) fn can_accept(&self) -> bool {
match self.task_acceptance {
TaskAcceptance::None => false,
TaskAcceptance::Single => {
matches!(self.active_slot, TaskSlotState::Empty) && self.pending_count == 0
}
TaskAcceptance::Queue => self.pending_count < self.queue_depth,
}
}
pub(crate) fn enqueue(&mut self, task_id: &str, context_id: &str) {
self.pending_count += 1;
self.history.insert(
task_id.to_string(),
(TaskState::Submitted, context_id.to_string()),
);
}
pub(crate) fn start_task(&mut self, task_id: String, context_id: String) {
debug_assert!(self.pending_count > 0);
self.pending_count -= 1;
self.history
.insert(task_id.clone(), (TaskState::Working, context_id.clone()));
self.active_slot = TaskSlotState::Running {
task_id,
context_id,
};
}
pub(crate) fn finish_task(&mut self, final_state: TaskState) {
if let TaskSlotState::Running {
ref task_id,
ref context_id,
} = self.active_slot
{
let (tid, cid) = (task_id.clone(), context_id.clone());
self.input_waiters.remove(&tid);
self.history.insert(tid.clone(), (final_state, cid));
self.active_slot = TaskSlotState::Done { task_id: tid };
}
}
pub(crate) fn set_input_required(
&mut self,
task_id: &str,
prompt: String,
tx: oneshot::Sender<String>,
) -> Result<(), &'static str> {
match &self.active_slot {
TaskSlotState::Running {
task_id: active_id, ..
} if active_id == task_id => {}
_ => return Err("task is not the active running task"),
}
let state = self.history.get(task_id).map(|(s, _)| s);
if !matches!(state, Some(TaskState::Working)) {
return Err("task is not in working state");
}
if let Some((_, ctx)) = self.history.get(task_id).cloned() {
self.history
.insert(task_id.to_string(), (TaskState::InputRequired, ctx));
}
self.input_waiters.insert(task_id.to_string(), (prompt, tx));
Ok(())
}
pub(crate) fn deliver_input(
&mut self,
task_id: &str,
text: String,
) -> Result<(), &'static str> {
let state = self.history.get(task_id).map(|(s, _)| s);
if !matches!(state, Some(TaskState::InputRequired)) {
return Err("task is not in input-required state");
}
let Some((_, tx)) = self.input_waiters.remove(task_id) else {
return Err("no input waiter found for task");
};
if let Some((_, ctx)) = self.history.get(task_id).cloned() {
self.history
.insert(task_id.to_string(), (TaskState::Working, ctx));
}
let _ = tx.send(text);
Ok(())
}
pub(crate) fn active_input_required_task_id(&self) -> Option<String> {
if let TaskSlotState::Running { ref task_id, .. } = self.active_slot {
let state = self.history.get(task_id).map(|(s, _)| s);
if matches!(state, Some(TaskState::InputRequired)) {
return Some(task_id.clone());
}
}
None
}
#[allow(dead_code)] pub(crate) fn get_input_prompt(&self, task_id: &str) -> Option<&str> {
self.input_waiters
.get(task_id)
.map(|(prompt, _)| prompt.as_str())
}
pub(crate) fn get_task(&self, task_id: &str) -> Option<A2aTask> {
self.history.get(task_id).map(|(state, context_id)| {
let artifacts = if matches!(state, TaskState::InputRequired) {
self.input_waiters.get(task_id).map(|(prompt, _)| {
vec![A2aArtifact {
name: "prompt".to_string(),
parts: vec![ArtifactPart {
text: prompt.clone(),
}],
}]
})
} else {
None
};
A2aTask {
id: task_id.to_string(),
context_id: context_id.clone(),
status: TaskStatus {
state: state.clone(),
},
artifacts,
}
})
}
}
#[derive(Debug)]
pub(crate) struct IncomingTask {
pub task_id: String,
pub context_id: String,
pub message_id: String,
pub message_text: String,
pub traceparent: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_registry() -> TaskRegistry {
TaskRegistry::new(1, TaskAcceptance::Single)
}
fn running_registry(task_id: &str) -> TaskRegistry {
let mut r = make_registry();
r.enqueue(task_id, "ctx_001");
r.start_task(task_id.to_string(), "ctx_001".to_string());
r
}
#[test]
fn set_input_required_transitions_state() {
let mut r = running_registry("tsk_001");
let (tx, _rx) = oneshot::channel();
assert!(r
.set_input_required("tsk_001", "which branch?".into(), tx)
.is_ok());
let task = r.get_task("tsk_001").unwrap();
assert_eq!(task.status.state, TaskState::InputRequired);
let artifacts = task.artifacts.unwrap();
assert_eq!(artifacts[0].name, "prompt");
assert_eq!(artifacts[0].parts[0].text, "which branch?");
}
#[test]
fn set_input_required_fails_for_wrong_task() {
let mut r = running_registry("tsk_001");
let (tx, _rx) = oneshot::channel();
assert!(r
.set_input_required("tsk_other", "prompt".into(), tx)
.is_err());
}
#[test]
fn deliver_input_transitions_back_to_working() {
let mut r = running_registry("tsk_001");
let (tx, mut rx) = oneshot::channel();
r.set_input_required("tsk_001", "which branch?".into(), tx)
.unwrap();
r.deliver_input("tsk_001", "main".to_string()).unwrap();
assert_eq!(rx.try_recv().unwrap(), "main");
let task = r.get_task("tsk_001").unwrap();
assert_eq!(task.status.state, TaskState::Working);
assert!(task.artifacts.is_none());
}
#[test]
fn deliver_input_fails_when_not_input_required() {
let mut r = running_registry("tsk_001");
assert!(r.deliver_input("tsk_001", "text".into()).is_err());
}
#[test]
fn active_input_required_task_id_returns_correct() {
let mut r = running_registry("tsk_001");
assert_eq!(r.active_input_required_task_id(), None);
let (tx, _rx) = oneshot::channel();
r.set_input_required("tsk_001", "prompt".into(), tx)
.unwrap();
assert_eq!(
r.active_input_required_task_id(),
Some("tsk_001".to_string())
);
r.deliver_input("tsk_001", "answer".into()).unwrap();
assert_eq!(r.active_input_required_task_id(), None);
}
#[test]
fn get_task_includes_artifacts_only_for_input_required() {
let mut r = running_registry("tsk_001");
let task = r.get_task("tsk_001").unwrap();
assert!(
task.artifacts.is_none(),
"working task should have no artifacts"
);
let (tx, _rx) = oneshot::channel();
r.set_input_required("tsk_001", "my prompt".into(), tx)
.unwrap();
let task = r.get_task("tsk_001").unwrap();
let artifacts = task.artifacts.unwrap();
assert_eq!(artifacts.len(), 1);
assert_eq!(artifacts[0].parts[0].text, "my prompt");
}
#[test]
fn finish_task_cleans_up_input_waiters() {
let mut r = running_registry("tsk_001");
let (tx, _rx) = oneshot::channel();
r.set_input_required("tsk_001", "prompt".into(), tx)
.unwrap();
r.finish_task(TaskState::Failed);
assert!(r.get_input_prompt("tsk_001").is_none());
}
}