use serde_json::Value;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultType {
Complete,
InputRequired,
Task,
Unknown(String),
Absent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskStatus {
Working,
InputRequired,
Completed,
Failed,
Cancelled,
Unknown(String),
}
impl TaskStatus {
#[must_use]
pub fn parse(raw: &str) -> Self {
match raw {
"working" => Self::Working,
"input_required" => Self::InputRequired,
"completed" => Self::Completed,
"failed" => Self::Failed,
"cancelled" => Self::Cancelled,
other => Self::Unknown(other.to_owned()),
}
}
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
}
#[must_use]
pub const fn delivered(&self) -> bool {
matches!(self, Self::Completed)
}
}
impl fmt::Display for TaskStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Working => "working",
Self::InputRequired => "input_required",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Cancelled => "cancelled",
Self::Unknown(s) => s,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RequestPeek {
pub is_continuation: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponsePeek {
pub result_type: ResultType,
pub is_error: bool,
pub task: Option<TaskPeek>,
}
impl ResponsePeek {
#[must_use]
pub fn with_legacy_delivery(mut self, method: &crate::Method) -> Self {
if method.delivers_priced_work()
&& matches!(self.result_type, ResultType::Absent)
&& !self.is_error
{
self.result_type = ResultType::Complete;
}
self
}
#[must_use]
pub const fn unrecognized() -> Self {
Self {
result_type: ResultType::Absent,
is_error: false,
task: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskPeek {
pub task_id: Option<String>,
pub status: TaskStatus,
}
#[must_use]
pub fn request(body: &Value) -> RequestPeek {
let Some(params) = body.get("params") else {
return RequestPeek::default();
};
RequestPeek {
is_continuation: params.get("inputResponses").is_some_and(|v| !v.is_null())
|| params.get("requestState").is_some_and(|v| !v.is_null()),
}
}
#[must_use]
pub fn response(body: &Value) -> ResponsePeek {
if body.get("error").is_some_and(|v| !v.is_null()) {
return ResponsePeek {
result_type: ResultType::Absent,
is_error: true,
task: None,
};
}
let Some(result) = body.get("result") else {
return ResponsePeek::unrecognized();
};
let result_type = match result.get("resultType").and_then(Value::as_str) {
Some("complete") => ResultType::Complete,
Some("input_required") => ResultType::InputRequired,
Some("task") => ResultType::Task,
Some(other) => ResultType::Unknown(other.to_owned()),
None => ResultType::Absent,
};
ResponsePeek {
result_type,
is_error: false,
task: peek_task(result),
}
}
fn peek_task(result: &Value) -> Option<TaskPeek> {
let status = result.get("status").and_then(Value::as_str)?;
Some(TaskPeek {
task_id: result
.get("taskId")
.and_then(Value::as_str)
.map(str::to_owned),
status: TaskStatus::parse(status),
})
}
#[cfg(test)]
mod tests {
use crate::{Call, Charge, Method, PriceBook, decide};
fn bare_result() -> serde_json::Value {
json!({"jsonrpc": "2.0", "id": 1, "result": {}})
}
#[test]
fn the_legacy_promotion_reaches_only_methods_that_deliver_priced_work() {
for method in ["tools/call", "resources/read", "prompts/get"] {
let promoted = response(&bare_result()).with_legacy_delivery(&Method::parse(method));
assert_eq!(
promoted.result_type,
ResultType::Complete,
"{method} delivers priced work and must still be promoted"
);
}
for method in [
"ping",
"initialize",
"notifications/initialized",
"logging/setLevel",
"resources/subscribe",
"completion/complete",
"vendor/extension",
"tools/list",
"tasks/get",
] {
let promoted = response(&bare_result()).with_legacy_delivery(&Method::parse(method));
assert_eq!(
promoted.result_type,
ResultType::Absent,
"{method} delivers no priced work and must not be promoted"
);
}
}
#[test]
fn an_idle_legacy_client_is_billed_nothing_for_its_keepalives() {
let prices = PriceBook::flat(1);
for method in ["ping", "initialize", "logging/setLevel"] {
let parsed = Method::parse(method);
let observed = response(&bare_result()).with_legacy_delivery(&parsed);
let charge = decide(&Call::new(parsed, None), &observed, &prices);
assert!(
matches!(charge, Charge::Free(_)),
"{method} billed {charge:?} on the legacy path"
);
}
}
#[test]
fn a_legacy_tool_call_is_still_billed() {
let observed = response(&bare_result()).with_legacy_delivery(&Method::ToolsCall);
let charge = decide(
&Call::new(Method::ToolsCall, Some("search".to_owned())),
&observed,
&PriceBook::flat(3),
);
assert!(matches!(charge, Charge::Billable(b) if b.units == 3));
}
#[test]
fn a_legacy_error_is_never_promoted() {
let error = json!({"jsonrpc": "2.0", "id": 1, "error": {"code": -32603, "message": "x"}});
let promoted = response(&error).with_legacy_delivery(&Method::ToolsCall);
assert!(promoted.is_error);
assert_eq!(promoted.result_type, ResultType::Absent);
}
use super::*;
use serde_json::json;
#[test]
fn a_fresh_tool_call_is_not_a_continuation() {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "Seattle, WA" } }
});
assert!(!request(&body).is_continuation);
}
#[test]
fn input_responses_mark_a_continuation() {
let body = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "New York" },
"inputResponses": {
"github_login": { "action": "accept", "content": { "name": "octocat" } }
},
"requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0..."
}
});
assert!(request(&body).is_continuation);
}
#[test]
fn request_state_alone_marks_a_continuation() {
let body = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": { "name": "t", "requestState": "AEAD-protected blob" }
});
assert!(request(&body).is_continuation);
}
#[test]
fn we_never_look_inside_request_state() {
for state in [
json!("AEAD-protected blob"),
json!(""),
json!("////not base64"),
] {
let body = json!({ "method": "tools/call", "params": { "requestState": state } });
assert!(request(&body).is_continuation);
}
}
#[test]
fn null_markers_do_not_count_as_present() {
let body = json!({
"method": "tools/call",
"params": { "name": "t", "inputResponses": null, "requestState": null }
});
assert!(!request(&body).is_continuation);
}
#[test]
fn a_body_without_params_is_not_a_continuation() {
assert!(!request(&json!({ "method": "tools/list" })).is_continuation);
assert!(!request(&json!({})).is_continuation);
}
#[test]
fn reads_the_three_result_types() {
let complete = json!({ "id": 2, "result": { "resultType": "complete", "content": [] } });
assert_eq!(response(&complete).result_type, ResultType::Complete);
let interim = json!({
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": { "github_login": { "method": "elicitation/create" } },
"requestState": "AEAD-protected blob"
}
});
assert_eq!(response(&interim).result_type, ResultType::InputRequired);
let task = json!({
"id": 1,
"result": { "resultType": "task", "taskId": "tsk_1", "status": "working", "ttlMs": 60000 }
});
assert_eq!(response(&task).result_type, ResultType::Task);
}
#[test]
fn errors_are_distinguished_from_results() {
let err = json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32020, "message": "Header mismatch" }
});
let peek = response(&err);
assert!(peek.is_error);
assert_eq!(peek.result_type, ResultType::Absent);
}
#[test]
fn unknown_result_types_are_preserved_not_coerced() {
let body = json!({ "result": { "resultType": "some_future_shape" } });
assert_eq!(
response(&body).result_type,
ResultType::Unknown("some_future_shape".to_owned())
);
}
#[test]
fn task_polls_carry_status_and_id() {
let working = json!({
"result": { "resultType": "complete", "taskId": "tsk_abc", "status": "working" }
});
let peek = response(&working);
let task = peek.task.expect("task present");
assert_eq!(task.status, TaskStatus::Working);
assert_eq!(task.task_id.as_deref(), Some("tsk_abc"));
assert!(!task.status.is_terminal());
assert!(!task.status.delivered());
}
#[test]
fn terminal_statuses_are_classified_correctly() {
assert!(TaskStatus::Completed.is_terminal() && TaskStatus::Completed.delivered());
assert!(TaskStatus::Failed.is_terminal() && !TaskStatus::Failed.delivered());
assert!(TaskStatus::Cancelled.is_terminal() && !TaskStatus::Cancelled.delivered());
assert!(!TaskStatus::Working.is_terminal() && !TaskStatus::Working.delivered());
assert!(!TaskStatus::InputRequired.is_terminal());
let unknown = TaskStatus::Unknown("paused".to_owned());
assert!(!unknown.is_terminal() && !unknown.delivered());
}
#[test]
fn task_status_round_trips_through_display() {
for raw in [
"working",
"input_required",
"completed",
"failed",
"cancelled",
] {
assert_eq!(TaskStatus::parse(raw).to_string(), raw);
}
}
#[test]
fn a_plain_result_carries_no_task() {
let body = json!({ "result": { "resultType": "complete", "content": [] } });
assert!(response(&body).task.is_none());
}
#[test]
fn a_response_with_neither_result_nor_error_is_unrecognized() {
assert_eq!(
response(&json!({ "jsonrpc": "2.0" })),
ResponsePeek::unrecognized()
);
}
}