The `#[serde(default)]` on `CallToolResult.content` (added in #752) made all fields optional, causing `CallToolResult` to greedily match any JSON object during `#[serde(untagged)]` deserialization of `ServerResult`. Similarly, `GetTaskPayloadResult(Value)` matched everything before `CustomResult(Value)` could be reached. Fix by replacing derived `Deserialize` impls with custom ones: - `CallToolResult`: require at least one known field to be present - `GetTaskPayloadResult`: always fail (indistinguishable from `CustomResult` in JSON; construct programmatically via `::new()`)
191 lines
6.3 KiB
Rust
191 lines
6.3 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
use super::Meta;
|
|
|
|
/// Canonical task lifecycle status as defined by SEP-1686.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub enum TaskStatus {
|
|
/// The receiver accepted the request and is currently working on it.
|
|
#[default]
|
|
Working,
|
|
/// The receiver requires additional input before work can continue.
|
|
InputRequired,
|
|
/// The underlying operation completed successfully and the result is ready.
|
|
Completed,
|
|
/// The underlying operation failed and will not continue.
|
|
Failed,
|
|
/// The task was cancelled and will not continue processing.
|
|
Cancelled,
|
|
}
|
|
|
|
/// Primary Task object that surfaces metadata during the task lifecycle.
|
|
///
|
|
/// Per spec, `lastUpdatedAt` and `ttl` are required fields.
|
|
/// `ttl` is nullable (`null` means unlimited retention).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
#[non_exhaustive]
|
|
pub struct Task {
|
|
/// Unique task identifier generated by the receiver.
|
|
pub task_id: String,
|
|
/// Current lifecycle status (see [`TaskStatus`]).
|
|
pub status: TaskStatus,
|
|
/// Optional human-readable status message for UI surfaces.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub status_message: Option<String>,
|
|
/// ISO-8601 creation timestamp.
|
|
pub created_at: String,
|
|
/// ISO-8601 timestamp for the most recent status change.
|
|
pub last_updated_at: String,
|
|
/// Retention window in milliseconds that the receiver agreed to honor.
|
|
/// `None` (serialized as `null`) means unlimited retention.
|
|
pub ttl: Option<u64>,
|
|
/// Suggested polling interval (milliseconds).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub poll_interval: Option<u64>,
|
|
}
|
|
|
|
impl Task {
|
|
/// Create a new Task with required fields.
|
|
pub fn new(
|
|
task_id: String,
|
|
status: TaskStatus,
|
|
created_at: String,
|
|
last_updated_at: String,
|
|
) -> Self {
|
|
Self {
|
|
task_id,
|
|
status,
|
|
status_message: None,
|
|
created_at,
|
|
last_updated_at,
|
|
ttl: None,
|
|
poll_interval: None,
|
|
}
|
|
}
|
|
|
|
/// Set the status message.
|
|
pub fn with_status_message(mut self, status_message: impl Into<String>) -> Self {
|
|
self.status_message = Some(status_message.into());
|
|
self
|
|
}
|
|
|
|
/// Set the TTL in milliseconds. `None` means unlimited retention.
|
|
pub fn with_ttl(mut self, ttl: u64) -> Self {
|
|
self.ttl = Some(ttl);
|
|
self
|
|
}
|
|
|
|
/// Set the poll interval in milliseconds.
|
|
pub fn with_poll_interval(mut self, poll_interval: u64) -> Self {
|
|
self.poll_interval = Some(poll_interval);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
#[non_exhaustive]
|
|
pub struct CreateTaskResult {
|
|
pub task: Task,
|
|
}
|
|
|
|
impl CreateTaskResult {
|
|
/// Create a new CreateTaskResult.
|
|
pub fn new(task: Task) -> Self {
|
|
Self { task }
|
|
}
|
|
}
|
|
|
|
/// Response to a `tasks/get` request.
|
|
///
|
|
/// Per spec, `GetTaskResult = allOf[Result, Task]` — the Task fields are
|
|
/// flattened at the top level, not nested under a `task` key.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub struct GetTaskResult {
|
|
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
|
pub meta: Option<Meta>,
|
|
#[serde(flatten)]
|
|
pub task: Task,
|
|
}
|
|
|
|
/// Response to a `tasks/result` request.
|
|
///
|
|
/// Per spec, the result structure matches the original request type
|
|
/// (e.g., `CallToolResult` for `tools/call`). This is represented as
|
|
/// an open object. The payload is the original request's result
|
|
/// serialized as a JSON value.
|
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
#[non_exhaustive]
|
|
pub struct GetTaskPayloadResult(pub Value);
|
|
|
|
impl GetTaskPayloadResult {
|
|
/// Create a new GetTaskPayloadResult with the given value.
|
|
pub fn new(value: Value) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
// Custom Deserialize that always fails, so that `GetTaskPayloadResult` is skipped
|
|
// during `#[serde(untagged)]` enum deserialization (e.g. `ServerResult`).
|
|
// The payload has the same JSON shape as `CustomResult(Value)`, so they are
|
|
// indistinguishable. `CustomResult` acts as the catch-all instead.
|
|
// `GetTaskPayloadResult` should be constructed programmatically via `::new()`.
|
|
impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
// Consume the value so the deserializer state stays consistent.
|
|
serde::de::IgnoredAny::deserialize(deserializer)?;
|
|
Err(serde::de::Error::custom(
|
|
"GetTaskPayloadResult cannot be deserialized directly; \
|
|
use CustomResult as the catch-all",
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Response to a `tasks/cancel` request.
|
|
///
|
|
/// Per spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub struct CancelTaskResult {
|
|
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
|
pub meta: Option<Meta>,
|
|
#[serde(flatten)]
|
|
pub task: Task,
|
|
}
|
|
|
|
/// Paginated list of tasks
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub struct TaskList {
|
|
pub tasks: Vec<Task>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub next_cursor: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub total: Option<u64>,
|
|
}
|
|
|
|
impl TaskList {
|
|
/// Create a new TaskList.
|
|
pub fn new(tasks: Vec<Task>) -> Self {
|
|
Self {
|
|
tasks,
|
|
next_cursor: None,
|
|
total: None,
|
|
}
|
|
}
|
|
}
|