use std::{
collections::{HashMap, HashSet},
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use murmur_artifact::TaskAcceptance;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::oneshot;
use crate::{cancel::CancelSignal, lanes::TaskLane, origin::TaskProvenance};
#[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,
Canceled,
}
impl TaskState {
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::Submitted => "submitted",
Self::Working => "working",
Self::InputRequired => "input-required",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Rejected => "rejected",
Self::Canceled => "canceled",
}
}
pub(crate) fn is_terminal(&self) -> bool {
match self {
Self::Submitted | Self::Working | Self::InputRequired => false,
Self::Completed | Self::Failed | Self::Rejected | Self::Canceled => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CancelOutcome {
Accepted,
AlreadyTerminal,
Unknown,
}
#[derive(Debug, Clone)]
pub(crate) enum TaskSlotState {
Empty,
Running {
task_id: String,
context_id: String,
lane: TaskLane,
},
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>)>,
input_timeouts: HashSet<String>,
cancels: HashMap<String, CancelSignal>,
resource_generation: Arc<AtomicU64>,
closed: bool,
}
pub(crate) const REJECTED_SESSION_ENDED_MESSAGE: &str =
"task rejected: the session ended before this task started";
pub(crate) const REJECTED_SESSION_STOPPED_MESSAGE: &str =
"task rejected: the session was stopped before this task started";
pub(crate) const REJECTED_SESSION_CLOSING_MESSAGE: &str = "task rejected: the session is closing";
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(),
input_timeouts: HashSet::new(),
cancels: HashMap::new(),
resource_generation: Arc::new(AtomicU64::new(0)),
closed: false,
}
}
pub(crate) fn resource_generation(&self) -> Arc<AtomicU64> {
Arc::clone(&self.resource_generation)
}
pub(crate) fn advance_resource_generation(&self) {
self.resource_generation.fetch_add(1, Ordering::SeqCst);
}
pub(crate) fn can_accept(&self) -> bool {
if self.closed {
return false;
}
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, lane: TaskLane) {
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,
lane,
};
}
pub(crate) fn active_lane(&self) -> Option<TaskLane> {
match self.active_slot {
TaskSlotState::Running { lane, .. } => Some(lane),
TaskSlotState::Empty | TaskSlotState::Done { .. } => None,
}
}
pub(crate) fn finish_task(&mut self, final_state: TaskState) -> Option<TaskState> {
let TaskSlotState::Running {
ref task_id,
ref context_id,
..
} = self.active_slot
else {
return None;
};
let (tid, cid) = (task_id.clone(), context_id.clone());
self.input_waiters.remove(&tid);
self.input_timeouts.remove(&tid);
self.cancels.remove(&tid);
let final_state = match self.history.get(&tid) {
Some((TaskState::Canceled, _)) => TaskState::Canceled,
_ => final_state,
};
self.history.insert(tid.clone(), (final_state.clone(), cid));
self.active_slot = TaskSlotState::Done { task_id: tid };
Some(final_state)
}
pub(crate) fn record_input_timeout(&mut self, task_id: &str) {
self.input_waiters.remove(task_id);
if let Some((TaskState::InputRequired, ctx)) = self.history.get(task_id).cloned() {
self.history
.insert(task_id.to_string(), (TaskState::Working, ctx));
}
self.input_timeouts.insert(task_id.to_string());
}
pub(crate) fn take_input_timeout(&mut self, task_id: &str) -> bool {
self.input_timeouts.remove(task_id)
}
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
}
pub(crate) fn cancel_watch(&mut self, task_id: &str) -> CancelSignal {
self.cancels.entry(task_id.to_string()).or_default().clone()
}
pub(crate) fn is_canceled(&self, task_id: &str) -> bool {
matches!(self.history.get(task_id), Some((TaskState::Canceled, _)))
}
pub(crate) fn request_cancel(&mut self, task_id: &str) -> CancelOutcome {
let Some((state, context_id)) = self.history.get(task_id).cloned() else {
return CancelOutcome::Unknown;
};
if state.is_terminal() {
return CancelOutcome::AlreadyTerminal;
}
if matches!(state, TaskState::Submitted) {
self.pending_count = self.pending_count.saturating_sub(1);
}
self.history
.insert(task_id.to_string(), (TaskState::Canceled, context_id));
self.cancel_watch(task_id).cancel();
CancelOutcome::Accepted
}
pub(crate) fn cancel_every_live(&mut self) -> Vec<String> {
let live: Vec<String> = self
.history
.iter()
.filter(|(_, (state, _))| !state.is_terminal())
.map(|(task_id, _)| task_id.clone())
.collect();
let mut canceled: Vec<String> = live
.into_iter()
.filter(|task_id| matches!(self.request_cancel(task_id), CancelOutcome::Accepted))
.collect();
canceled.sort();
canceled
}
pub(crate) fn close_to_new_work(&mut self) -> Vec<(String, String)> {
self.closed = true;
let mut refused: Vec<(String, String)> = self
.history
.iter()
.filter(|(_, (state, _))| matches!(state, TaskState::Submitted))
.map(|(task_id, (_, context_id))| (task_id.clone(), context_id.clone()))
.collect();
refused.sort();
for (task_id, context_id) in &refused {
self.history
.insert(task_id.clone(), (TaskState::Rejected, context_id.clone()));
self.cancels.remove(task_id);
}
self.pending_count = self.pending_count.saturating_sub(refused.len());
refused
}
pub(crate) fn is_closed(&self) -> bool {
self.closed
}
#[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>,
pub provenance: TaskProvenance,
pub source: &'static str,
pub delegation_id: Option<String>,
pub forget_session: bool,
}
pub(crate) const SOURCE_A2A: &str = "a2a";
pub(crate) const SOURCE_DETACHED_SHELL: &str = "detached_shell";
pub(crate) const SOURCE_DETACHED_LOST: &str = "detached_lost";
#[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(), TaskLane::Bg);
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 request_cancel_on_a_running_task_records_canceled() {
let mut r = running_registry("tsk_001");
let signal = r.cancel_watch("tsk_001");
assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
assert!(signal.is_canceled());
assert!(r.is_canceled("tsk_001"));
let task = r.get_task("tsk_001").unwrap();
assert_eq!(task.status.state, TaskState::Canceled);
}
#[test]
fn request_cancel_on_a_completed_task_leaves_it_alone() {
let mut r = running_registry("tsk_001");
r.finish_task(TaskState::Completed);
assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::AlreadyTerminal);
assert_eq!(
r.get_task("tsk_001").unwrap().status.state,
TaskState::Completed
);
let mut r = running_registry("tsk_002");
assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::Accepted);
assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::AlreadyTerminal);
}
#[test]
fn cancel_every_live_cancels_live_tasks_in_order_and_leaves_terminal_ones() {
let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
r.enqueue("tsk_c", "ctx_001");
r.enqueue("tsk_a", "ctx_001");
r.enqueue("tsk_done", "ctx_001");
r.start_task("tsk_done".to_string(), "ctx_001".to_string(), TaskLane::Bg);
r.finish_task(TaskState::Completed);
r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
let running = r.cancel_watch("tsk_a");
assert_eq!(r.cancel_every_live(), vec!["tsk_a", "tsk_c"]);
assert!(running.is_canceled());
assert!(r.is_canceled("tsk_c"));
assert_eq!(
r.get_task("tsk_done").unwrap().status.state,
TaskState::Completed
);
assert!(r.cancel_every_live().is_empty());
}
#[test]
fn request_cancel_on_an_unknown_id_is_unknown() {
let mut r = make_registry();
assert_eq!(r.request_cancel("tsk_doesnotexist"), CancelOutcome::Unknown);
assert!(!r.is_canceled("tsk_doesnotexist"));
}
#[test]
fn finish_task_does_not_overwrite_an_accepted_cancel() {
let mut r = running_registry("tsk_001");
assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
assert_eq!(
r.finish_task(TaskState::Completed),
Some(TaskState::Canceled),
"the state recorded is the accepted cancel, and the caller is told so"
);
assert_eq!(
r.get_task("tsk_001").unwrap().status.state,
TaskState::Canceled
);
assert_eq!(
r.finish_task(TaskState::Failed),
None,
"a slot that is no longer running records nothing"
);
assert_eq!(
r.get_task("tsk_001").unwrap().status.state,
TaskState::Canceled
);
}
#[test]
fn finish_task_returns_the_state_it_recorded() {
let mut r = running_registry("tsk_001");
assert_eq!(
r.finish_task(TaskState::Completed),
Some(TaskState::Completed)
);
assert_eq!(make_registry().finish_task(TaskState::Failed), None);
}
#[test]
fn an_input_timeout_leaves_the_task_working_and_is_read_once() {
let mut r = running_registry("tsk_001");
let (tx, _rx) = oneshot::channel();
r.set_input_required("tsk_001", "prompt".into(), tx)
.unwrap();
r.record_input_timeout("tsk_001");
assert_eq!(
r.get_task("tsk_001").unwrap().status.state,
TaskState::Working,
"the task's terminal state is not the wait's to record"
);
assert!(
r.get_input_prompt("tsk_001").is_none(),
"the waiter is gone"
);
assert_eq!(r.active_input_required_task_id(), None);
assert!(r.take_input_timeout("tsk_001"));
assert!(
!r.take_input_timeout("tsk_001"),
"a reopened attempt does not end on the same timeout"
);
assert!(!r.take_input_timeout("tsk_other"));
}
#[test]
fn cancelling_a_queued_task_frees_its_queue_slot() {
let mut r = TaskRegistry::new(2, TaskAcceptance::Queue);
r.enqueue("tsk_a", "ctx_001");
r.enqueue("tsk_b", "ctx_001");
assert!(!r.can_accept(), "the queue is full");
assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::Accepted);
assert!(r.can_accept(), "a cancelled task holds no slot");
}
#[test]
fn close_to_new_work_rejects_every_submitted_task_and_nothing_else() {
let mut r = TaskRegistry::new(4, TaskAcceptance::Queue);
for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_d"] {
r.enqueue(id, &format!("ctx_{id}"));
}
r.start_task("tsk_a".to_string(), "ctx_tsk_a".to_string(), TaskLane::Bg);
let (tx, _rx) = oneshot::channel();
r.set_input_required("tsk_a", "prompt".into(), tx).unwrap();
assert_eq!(r.request_cancel("tsk_c"), CancelOutcome::Accepted);
let pending_before = r.pending_count;
let refused = r.close_to_new_work();
assert_eq!(
refused,
vec![
("tsk_b".to_string(), "ctx_tsk_b".to_string()),
("tsk_d".to_string(), "ctx_tsk_d".to_string()),
]
);
for id in ["tsk_b", "tsk_d"] {
assert_eq!(r.get_task(id).unwrap().status.state, TaskState::Rejected);
}
assert_eq!(r.pending_count, pending_before - 2);
assert_eq!(r.pending_count, 0);
assert_eq!(
r.get_task("tsk_a").unwrap().status.state,
TaskState::InputRequired,
"a live task that is not submitted is left alone"
);
assert_eq!(
r.get_task("tsk_c").unwrap().status.state,
TaskState::Canceled
);
assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::AlreadyTerminal);
assert!(!r.is_canceled("tsk_b"));
}
#[test]
fn a_closed_registry_accepts_nothing_and_closes_once() {
for (depth, mode) in [(1, TaskAcceptance::Single), (4, TaskAcceptance::Queue)] {
let mut r = TaskRegistry::new(depth, mode.clone());
assert!(r.can_accept());
assert!(!r.is_closed());
r.enqueue("tsk_a", "ctx_001");
assert_eq!(r.close_to_new_work().len(), 1);
assert!(r.is_closed());
assert!(!r.can_accept(), "{mode:?}: a closed registry refuses work");
assert!(r.close_to_new_work().is_empty(), "{mode:?}: closes once");
assert!(!r.can_accept());
}
}
#[test]
fn as_str_is_the_serialized_spelling() {
for state in [
TaskState::Submitted,
TaskState::Working,
TaskState::InputRequired,
TaskState::Completed,
TaskState::Failed,
TaskState::Rejected,
TaskState::Canceled,
] {
assert_eq!(
serde_json::to_value(&state).unwrap(),
serde_json::json!(state.as_str())
);
}
}
#[test]
fn canceled_serializes_with_the_protocol_spelling() {
let json = serde_json::to_string(&TaskState::Canceled).unwrap();
assert_eq!(json, "\"canceled\"");
assert_eq!(
serde_json::to_string(&TaskState::InputRequired).unwrap(),
"\"input-required\""
);
}
#[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());
}
}