use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use chrono::{DateTime, SecondsFormat};
use polyc_rpc_client::{
AgentTaskRecord, AgentTaskState, AgentTaskTransition, DialError, ErrorCode, TaskDialer,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::types::{Artifact, Message, Role, Task, TaskState, TaskStatus};
pub const MAX_ID_BYTES: usize = 256;
pub const MAX_METADATA_BYTES: usize = 8 * 1024;
pub const MAX_PAGE_SIZE: usize = 1000;
const DURABLE_PAGE_SIZE: usize = 100;
type StoreFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, TaskStoreError>> + Send + 'a>>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TaskStoreError {
#[error("no task exists under this id")]
NotFound,
#[error("a task already exists under this id")]
AlreadyExists,
#[error("this task already finished")]
Terminal,
#[error("this task is already running")]
Running,
#[error("{0}")]
Refused(String),
#[error("{0}")]
Unavailable(String),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TaskPage {
pub tasks: Vec<Task>,
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TaskUpdate<'a> {
pub status: &'a TaskStatus,
pub artifacts: Option<&'a Vec<Artifact>>,
pub metadata: Option<&'a HashMap<String, Value>>,
pub appended_history: &'a [Message],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskOwnership {
pub dispatch_id: String,
pub worker_instance: String,
pub attempt_id: String,
pub fence: u64,
pub claim_digest: Vec<u8>,
}
pub trait TaskStore: Send + Sync {
fn create<'a>(&'a self, task: &'a Task) -> StoreFuture<'a, ()>;
fn claim<'a>(
&'a self,
task_id: &'a str,
appended_history: &'a [Message],
dispatch_id: &'a str,
) -> StoreFuture<'a, TaskOwnership>;
fn transition<'a>(
&'a self,
task_id: &'a str,
update: TaskUpdate<'a>,
ownership: Option<&'a TaskOwnership>,
) -> StoreFuture<'a, ()>;
fn renew<'a>(
&'a self,
task_id: &'a str,
ownership: &'a TaskOwnership,
ordinal: u64,
) -> StoreFuture<'a, ()>;
fn cancel<'a>(&'a self, task_id: &'a str) -> StoreFuture<'a, Task>;
fn get<'a>(&'a self, task_id: &'a str) -> StoreFuture<'a, Option<Task>>;
fn list<'a>(
&'a self,
context_id: &'a str,
page_size: usize,
page_token: Option<&'a str>,
) -> StoreFuture<'a, TaskPage>;
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MessageFrame {
message_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
context_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
task_id: Option<String>,
role: Role,
}
impl From<&Message> for MessageFrame {
fn from(message: &Message) -> Self {
Self {
message_id: message.message_id.clone(),
context_id: message.context_id.clone(),
task_id: message.task_id.clone(),
role: message.role,
}
}
}
impl From<MessageFrame> for Message {
fn from(frame: MessageFrame) -> Self {
Self {
message_id: frame.message_id,
context_id: frame.context_id,
task_id: frame.task_id,
role: frame.role,
parts: Vec::new(),
metadata: None,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ArtifactFrame {
artifact_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
}
impl From<&Artifact> for ArtifactFrame {
fn from(artifact: &Artifact) -> Self {
Self {
artifact_id: artifact.artifact_id.clone(),
name: artifact.name.clone(),
}
}
}
impl From<ArtifactFrame> for Artifact {
fn from(frame: ArtifactFrame) -> Self {
Self {
artifact_id: frame.artifact_id,
name: frame.name,
description: None,
parts: Vec::new(),
metadata: None,
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StatusDetailFrame {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<MessageFrame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
}
fn bounded_id(what: &str, value: &str) -> Result<(), TaskStoreError> {
if value.len() > MAX_ID_BYTES {
return Err(TaskStoreError::Refused(format!(
"a task record holds at most {MAX_ID_BYTES} bytes of {what}, and this one is {} bytes",
value.len()
)));
}
Ok(())
}
fn bounded_optional(what: &str, value: Option<&String>) -> Result<(), TaskStoreError> {
value.map_or(Ok(()), |value| bounded_id(what, value))
}
fn encode<T: Serialize>(what: &str, value: &T) -> Result<Vec<u8>, TaskStoreError> {
serde_json::to_vec(value).map_err(|err| {
TaskStoreError::Refused(format!("this task's {what} could not be saved: {err}"))
})
}
fn message_frame(message: &Message) -> Result<Vec<u8>, TaskStoreError> {
bounded_id("message id", &message.message_id)?;
bounded_optional("context id", message.context_id.as_ref())?;
bounded_optional("task id", message.task_id.as_ref())?;
encode("message", &MessageFrame::from(message))
}
fn artifact_frame(artifact: &Artifact) -> Result<Vec<u8>, TaskStoreError> {
bounded_id("artifact id", &artifact.artifact_id)?;
bounded_optional("artifact name", artifact.name.as_ref())?;
encode("artifact", &ArtifactFrame::from(artifact))
}
fn status_detail_frame(status: &TaskStatus) -> Result<Vec<u8>, TaskStoreError> {
let Some(detail) = status_detail(status)? else {
return Ok(Vec::new());
};
encode("status", &detail)
}
fn status_detail(status: &TaskStatus) -> Result<Option<StatusDetailFrame>, TaskStoreError> {
if status.message.is_none() && status.timestamp.is_none() {
return Ok(None);
}
if let Some(message) = status.message.as_ref() {
bounded_id("message id", &message.message_id)?;
bounded_optional("context id", message.context_id.as_ref())?;
bounded_optional("task id", message.task_id.as_ref())?;
}
Ok(Some(StatusDetailFrame {
message: status.message.as_ref().map(MessageFrame::from),
timestamp: status.timestamp.clone(),
}))
}
fn metadata_frame(metadata: Option<&HashMap<String, Value>>) -> Result<Vec<u8>, TaskStoreError> {
let Some(metadata) = metadata.filter(|metadata| !metadata.is_empty()) else {
return Ok(Vec::new());
};
let frame = encode("metadata", metadata)?;
if frame.len() > MAX_METADATA_BYTES {
return Err(TaskStoreError::Refused(format!(
"a task record holds metadata of at most {MAX_METADATA_BYTES} bytes, and this one is \
{} bytes",
frame.len()
)));
}
Ok(frame)
}
const fn from_record_state(state: AgentTaskState) -> TaskState {
match state {
AgentTaskState::Submitted => TaskState::Submitted,
AgentTaskState::Working => TaskState::Working,
AgentTaskState::InputRequired => TaskState::InputRequired,
AgentTaskState::AuthRequired => TaskState::AuthRequired,
AgentTaskState::Completed => TaskState::Completed,
AgentTaskState::Failed => TaskState::Failed,
AgentTaskState::Canceled => TaskState::Canceled,
AgentTaskState::Rejected => TaskState::Rejected,
}
}
const fn to_record_state(state: TaskState) -> Option<AgentTaskState> {
match state {
TaskState::Submitted => Some(AgentTaskState::Submitted),
TaskState::Working => Some(AgentTaskState::Working),
TaskState::InputRequired => Some(AgentTaskState::InputRequired),
TaskState::AuthRequired => Some(AgentTaskState::AuthRequired),
TaskState::Completed => Some(AgentTaskState::Completed),
TaskState::Failed => Some(AgentTaskState::Failed),
TaskState::Canceled => Some(AgentTaskState::Canceled),
TaskState::Rejected => Some(AgentTaskState::Rejected),
TaskState::Unspecified => None,
}
}
fn transition_timestamp(updated_at_ms: u64) -> Option<String> {
if updated_at_ms == 0 {
return None;
}
let millis = i64::try_from(updated_at_ms).ok()?;
DateTime::from_timestamp_millis(millis)
.map(|at| at.to_rfc3339_opts(SecondsFormat::Millis, true))
}
fn task_from_record(record: &AgentTaskRecord) -> Task {
let detail: StatusDetailFrame = if record.status_detail.is_empty() {
StatusDetailFrame::default()
} else {
serde_json::from_slice(&record.status_detail).unwrap_or_default()
};
let artifacts: Vec<Artifact> = record
.artifacts
.iter()
.filter_map(|frame| serde_json::from_slice::<ArtifactFrame>(frame).ok())
.map(Artifact::from)
.collect();
let history: Vec<Message> = record
.history
.iter()
.filter_map(|frame| serde_json::from_slice::<MessageFrame>(frame).ok())
.map(Message::from)
.collect();
let metadata: Option<HashMap<String, Value>> = if record.metadata.is_empty() {
None
} else {
serde_json::from_slice(&record.metadata).ok()
};
Task {
id: record.task_id.clone(),
context_id: record.context_id.clone(),
status: TaskStatus {
state: from_record_state(record.state),
message: detail.message.map(Message::from),
timestamp: detail
.timestamp
.or_else(|| transition_timestamp(record.updated_at_ms)),
},
artifacts: (!artifacts.is_empty()).then_some(artifacts),
history: (!history.is_empty()).then_some(history),
metadata,
}
}
fn from_dial_error(error: &DialError) -> TaskStoreError {
match error.code() {
Some(ErrorCode::NotFound) => TaskStoreError::NotFound,
Some(ErrorCode::AlreadyExists) => TaskStoreError::AlreadyExists,
Some(ErrorCode::FailedPrecondition) => TaskStoreError::Terminal,
Some(ErrorCode::Aborted) => TaskStoreError::Running,
Some(ErrorCode::InvalidArgument) => TaskStoreError::Refused(error.to_string()),
_ => TaskStoreError::Unavailable(error.to_string()),
}
}
#[derive(Clone)]
pub struct TaskDialerStore {
dialer: TaskDialer,
worker_instance: String,
}
impl TaskDialerStore {
pub fn new(addr: &str) -> Result<Self, DialError> {
Ok(Self {
dialer: TaskDialer::new(addr)?,
worker_instance: uuid::Uuid::now_v7().to_string(),
})
}
pub fn with_bearer(addr: &str, bearer: &str) -> Result<Self, DialError> {
Ok(Self {
dialer: TaskDialer::with_bearer(addr, bearer)?,
worker_instance: uuid::Uuid::now_v7().to_string(),
})
}
}
impl TaskStore for TaskDialerStore {
fn create<'a>(&'a self, task: &'a Task) -> StoreFuture<'a, ()> {
Box::pin(async move {
let history = task
.history
.as_deref()
.unwrap_or_default()
.iter()
.map(message_frame)
.collect::<Result<Vec<_>, _>>()?;
self.dialer
.create_task(&task.id, &task.context_id, history)
.await
.map(|_| ())
.map_err(|err| from_dial_error(&err))
})
}
fn claim<'a>(
&'a self,
task_id: &'a str,
appended_history: &'a [Message],
dispatch_id: &'a str,
) -> StoreFuture<'a, TaskOwnership> {
Box::pin(async move {
let attempt_id = uuid::Uuid::now_v7().to_string();
let (_, ownership) = self
.dialer
.claim_task(
task_id,
dispatch_id,
&self.worker_instance,
&attempt_id,
appended_history
.iter()
.map(message_frame)
.collect::<Result<Vec<_>, _>>()?,
)
.await
.map_err(|err| from_dial_error(&err))?;
Ok(TaskOwnership {
dispatch_id: ownership.dispatch_id,
worker_instance: ownership.worker_instance,
attempt_id: ownership.attempt_id,
fence: ownership.fence,
claim_digest: ownership.claim_digest,
})
})
}
fn transition<'a>(
&'a self,
task_id: &'a str,
update: TaskUpdate<'a>,
ownership: Option<&'a TaskOwnership>,
) -> StoreFuture<'a, ()> {
Box::pin(async move {
let ownership = ownership.ok_or_else(|| {
TaskStoreError::Refused(
"a claimed task update carries no State ownership".to_owned(),
)
})?;
let state = to_record_state(update.status.state).ok_or_else(|| {
TaskStoreError::Refused(
"this task cannot move to a state it does not name".to_owned(),
)
})?;
let successor = AgentTaskTransition {
status_detail: status_detail_frame(update.status)?,
artifacts: update
.artifacts
.map(|artifacts| {
artifacts
.iter()
.map(artifact_frame)
.collect::<Result<Vec<_>, _>>()
})
.transpose()?
.unwrap_or_default(),
metadata: metadata_frame(update.metadata)?,
appended_history: update
.appended_history
.iter()
.map(message_frame)
.collect::<Result<Vec<_>, _>>()?,
};
self.dialer
.transition_task(
task_id,
state,
successor,
&polyc_rpc_client::AgentTaskOwnership {
dispatch_id: ownership.dispatch_id.clone(),
worker_instance: ownership.worker_instance.clone(),
attempt_id: ownership.attempt_id.clone(),
fence: ownership.fence,
claim_digest: ownership.claim_digest.clone(),
},
)
.await
.map(|_| ())
.map_err(|err| from_dial_error(&err))
})
}
fn renew<'a>(
&'a self,
task_id: &'a str,
ownership: &'a TaskOwnership,
ordinal: u64,
) -> StoreFuture<'a, ()> {
Box::pin(async move {
self.dialer
.renew_task_claim(
task_id,
&polyc_rpc_client::AgentTaskOwnership {
dispatch_id: ownership.dispatch_id.clone(),
worker_instance: ownership.worker_instance.clone(),
attempt_id: ownership.attempt_id.clone(),
fence: ownership.fence,
claim_digest: ownership.claim_digest.clone(),
},
ordinal,
)
.await
.map_err(|err| from_dial_error(&err))
})
}
fn cancel<'a>(&'a self, task_id: &'a str) -> StoreFuture<'a, Task> {
Box::pin(async move {
self.dialer
.cancel_task(task_id)
.await
.map(|record| task_from_record(&record))
.map_err(|err| from_dial_error(&err))
})
}
fn get<'a>(&'a self, task_id: &'a str) -> StoreFuture<'a, Option<Task>> {
Box::pin(async move {
self.dialer
.get_task(task_id)
.await
.map(|record| record.as_ref().map(task_from_record))
.map_err(|err| from_dial_error(&err))
})
}
fn list<'a>(
&'a self,
context_id: &'a str,
page_size: usize,
page_token: Option<&'a str>,
) -> StoreFuture<'a, TaskPage> {
Box::pin(async move {
let page_size = u32::try_from(page_size.clamp(1, DURABLE_PAGE_SIZE)).unwrap_or(1);
self.dialer
.list_tasks(context_id, page_size, page_token)
.await
.map(|page| TaskPage {
tasks: page.tasks.iter().map(task_from_record).collect(),
next_page_token: page.next_after,
})
.map_err(|err| from_dial_error(&err))
})
}
}
const UNCONFIGURED_STORE_MESSAGE: &str = "control-plane address is unset \
(POLYCHROME_AGENT_ADDR); this edge serves its Agent Card but cannot record or read tasks \
until it is set";
#[derive(Clone, Copy, Debug, Default)]
pub struct UnconfiguredTaskStore;
impl UnconfiguredTaskStore {
fn refuse<T>() -> Result<T, TaskStoreError> {
Err(TaskStoreError::Unavailable(
UNCONFIGURED_STORE_MESSAGE.to_owned(),
))
}
}
impl TaskStore for UnconfiguredTaskStore {
fn create<'a>(&'a self, _task: &'a Task) -> StoreFuture<'a, ()> {
Box::pin(async { Self::refuse() })
}
fn claim<'a>(
&'a self,
_task_id: &'a str,
_appended_history: &'a [Message],
_dispatch_id: &'a str,
) -> StoreFuture<'a, TaskOwnership> {
Box::pin(async { Self::refuse() })
}
fn transition<'a>(
&'a self,
_task_id: &'a str,
_update: TaskUpdate<'a>,
_ownership: Option<&'a TaskOwnership>,
) -> StoreFuture<'a, ()> {
Box::pin(async { Self::refuse() })
}
fn renew<'a>(
&'a self,
_task_id: &'a str,
_ownership: &'a TaskOwnership,
_ordinal: u64,
) -> StoreFuture<'a, ()> {
Box::pin(async { Self::refuse() })
}
fn cancel<'a>(&'a self, _task_id: &'a str) -> StoreFuture<'a, Task> {
Box::pin(async { Self::refuse() })
}
fn get<'a>(&'a self, _task_id: &'a str) -> StoreFuture<'a, Option<Task>> {
Box::pin(async { Self::refuse() })
}
fn list<'a>(
&'a self,
_context_id: &'a str,
_page_size: usize,
_page_token: Option<&'a str>,
) -> StoreFuture<'a, TaskPage> {
Box::pin(async { Self::refuse() })
}
}
#[cfg(any(test, feature = "test-util"))]
pub mod test_double {
use std::collections::HashMap;
use std::sync::{
RwLock,
atomic::{AtomicU64, Ordering},
};
use super::{
Artifact, ArtifactFrame, Message, MessageFrame, StoreFuture, Task, TaskOwnership, TaskPage,
TaskStatus, TaskStore, TaskStoreError, TaskUpdate, artifact_frame, message_frame,
metadata_frame,
};
pub struct InMemoryTaskStore {
tasks: RwLock<HashMap<String, Task>>,
next_fence: AtomicU64,
outage: Option<String>,
}
impl Default for InMemoryTaskStore {
fn default() -> Self {
Self {
tasks: RwLock::default(),
next_fence: AtomicU64::new(1),
outage: None,
}
}
}
fn recorded_message(message: &Message) -> Result<Message, TaskStoreError> {
let frame = message_frame(message)?;
let decoded: MessageFrame = serde_json::from_slice(&frame)
.map_err(|err| TaskStoreError::Refused(err.to_string()))?;
Ok(Message::from(decoded))
}
fn recorded_artifact(artifact: &Artifact) -> Result<Artifact, TaskStoreError> {
let frame = artifact_frame(artifact)?;
let decoded: ArtifactFrame = serde_json::from_slice(&frame)
.map_err(|err| TaskStoreError::Refused(err.to_string()))?;
Ok(Artifact::from(decoded))
}
fn recorded_status(status: &TaskStatus) -> Result<TaskStatus, TaskStoreError> {
Ok(TaskStatus {
state: status.state,
message: status.message.as_ref().map(recorded_message).transpose()?,
timestamp: status.timestamp.clone(),
})
}
impl InMemoryTaskStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn unreachable(reason: &str) -> Self {
Self {
tasks: RwLock::default(),
next_fence: AtomicU64::new(1),
outage: Some(reason.to_owned()),
}
}
fn outage<T>(&self) -> Option<Result<T, TaskStoreError>> {
self.outage
.as_ref()
.map(|reason| Err(TaskStoreError::Unavailable(reason.clone())))
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, HashMap<String, Task>> {
self.tasks
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<String, Task>> {
self.tasks
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl TaskStore for InMemoryTaskStore {
fn create<'a>(&'a self, task: &'a Task) -> StoreFuture<'a, ()> {
Box::pin(async move {
if let Some(outage) = self.outage() {
return outage;
}
let history = task
.history
.as_deref()
.unwrap_or_default()
.iter()
.map(recorded_message)
.collect::<Result<Vec<_>, _>>()?;
let mut tasks = self.write();
{
if tasks.contains_key(&task.id) {
return Err(TaskStoreError::AlreadyExists);
}
let created = Task {
id: task.id.clone(),
context_id: task.context_id.clone(),
status: TaskStatus {
state: crate::types::TaskState::Submitted,
message: None,
timestamp: None,
},
artifacts: None,
history: Some(history),
metadata: None,
};
tasks.insert(created.id.clone(), created);
}
drop(tasks);
Ok(())
})
}
fn claim<'a>(
&'a self,
task_id: &'a str,
appended_history: &'a [Message],
dispatch_id: &'a str,
) -> StoreFuture<'a, TaskOwnership> {
Box::pin(async move {
if let Some(outage) = self.outage() {
return outage;
}
let appended = appended_history
.iter()
.map(recorded_message)
.collect::<Result<Vec<_>, _>>()?;
let mut tasks = self.write();
{
let task = tasks.get_mut(task_id).ok_or(TaskStoreError::NotFound)?;
if task.status.state != crate::types::TaskState::Submitted
&& !matches!(
task.status.state,
crate::types::TaskState::InputRequired
| crate::types::TaskState::AuthRequired
)
{
return Err(TaskStoreError::Running);
}
task.status = TaskStatus {
state: crate::types::TaskState::Working,
message: None,
timestamp: None,
};
let mut history = task.history.take().unwrap_or_default();
history.extend(appended);
task.history = Some(history);
}
drop(tasks);
Ok(TaskOwnership {
dispatch_id: dispatch_id.to_owned(),
worker_instance: "in-memory-worker".to_owned(),
attempt_id: uuid::Uuid::now_v7().to_string(),
fence: self.next_fence.fetch_add(1, Ordering::Relaxed),
claim_digest: vec![0; 32],
})
})
}
fn transition<'a>(
&'a self,
task_id: &'a str,
update: TaskUpdate<'a>,
_ownership: Option<&'a TaskOwnership>,
) -> StoreFuture<'a, ()> {
Box::pin(async move {
if let Some(outage) = self.outage() {
return outage;
}
let status = recorded_status(update.status)?;
let artifacts = update
.artifacts
.map(|artifacts| {
artifacts
.iter()
.map(recorded_artifact)
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
let metadata = metadata_frame(update.metadata)?;
let appended = update
.appended_history
.iter()
.map(recorded_message)
.collect::<Result<Vec<_>, _>>()?;
let mut tasks = self.write();
{
let task = tasks.get_mut(task_id).ok_or(TaskStoreError::NotFound)?;
if task.status.state.is_terminal() {
return Err(TaskStoreError::Terminal);
}
task.status = status;
task.artifacts = artifacts;
task.metadata = (!metadata.is_empty())
.then(|| update.metadata.cloned())
.flatten();
let mut history = task.history.take().unwrap_or_default();
history.extend(appended);
task.history = Some(history);
}
drop(tasks);
Ok(())
})
}
fn renew<'a>(
&'a self,
_task_id: &'a str,
_ownership: &'a TaskOwnership,
_ordinal: u64,
) -> StoreFuture<'a, ()> {
Box::pin(async { Ok(()) })
}
fn cancel<'a>(&'a self, task_id: &'a str) -> StoreFuture<'a, Task> {
Box::pin(async move {
if let Some(outage) = self.outage() {
return outage;
}
let mut tasks = self.write();
let canceled = {
let task = tasks.get_mut(task_id).ok_or(TaskStoreError::NotFound)?;
if task.status.state.is_terminal() {
return Err(TaskStoreError::Terminal);
}
task.status = TaskStatus {
state: crate::types::TaskState::Canceled,
message: None,
timestamp: None,
};
task.clone()
};
drop(tasks);
Ok(canceled)
})
}
fn get<'a>(&'a self, task_id: &'a str) -> StoreFuture<'a, Option<Task>> {
Box::pin(async move {
if let Some(outage) = self.outage() {
return outage;
}
Ok(self.read().get(task_id).cloned())
})
}
fn list<'a>(
&'a self,
context_id: &'a str,
page_size: usize,
page_token: Option<&'a str>,
) -> StoreFuture<'a, TaskPage> {
Box::pin(async move {
if let Some(outage) = self.outage() {
return outage;
}
let mut all: Vec<Task> = self
.read()
.values()
.filter(|task| task.context_id == context_id)
.filter(|task| page_token.is_none_or(|after| task.id.as_str() > after))
.cloned()
.collect();
all.sort_by(|a, b| a.id.cmp(&b.id));
let has_more = all.len() > page_size;
all.truncate(page_size);
let next_page_token = has_more.then(|| all.last().map(|t| t.id.clone())).flatten();
Ok(TaskPage {
tasks: all,
next_page_token,
})
})
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use crate::types::Part;
use test_double::InMemoryTaskStore;
fn message(text: &str) -> Message {
Message {
message_id: "m1".to_owned(),
context_id: Some("ctx-1".to_owned()),
task_id: Some("t1".to_owned()),
role: Role::Agent,
parts: vec![Part::text(text)],
metadata: None,
}
}
fn task(id: &str, ctx: &str) -> Task {
Task {
id: id.to_owned(),
context_id: ctx.to_owned(),
status: TaskStatus {
state: TaskState::Submitted,
message: None,
timestamp: None,
},
artifacts: None,
history: Some(vec![message("hello")]),
metadata: None,
}
}
#[test]
fn a_message_frame_keeps_its_lifecycle_and_drops_its_content() {
let frame = message_frame(&message("hello")).expect("encodes");
let decoded =
Message::from(serde_json::from_slice::<MessageFrame>(&frame).expect("decodes"));
assert_eq!(decoded.message_id, "m1");
assert_eq!(decoded.context_id.as_deref(), Some("ctx-1"));
assert_eq!(decoded.task_id.as_deref(), Some("t1"));
assert_eq!(decoded.role, Role::Agent);
assert!(decoded.parts.is_empty(), "no content is recorded");
assert!(
!String::from_utf8_lossy(&frame).contains("hello"),
"the text must not reach the record: {}",
String::from_utf8_lossy(&frame)
);
}
#[test]
fn an_artifact_frame_keeps_its_identity_and_drops_its_content() {
let artifact = Artifact {
artifact_id: "a1".to_owned(),
name: Some("answer".to_owned()),
description: Some("the long description".to_owned()),
parts: vec![Part::text("the answer text")],
metadata: None,
};
let frame = artifact_frame(&artifact).expect("encodes");
let decoded =
Artifact::from(serde_json::from_slice::<ArtifactFrame>(&frame).expect("decodes"));
assert_eq!(decoded.artifact_id, "a1");
assert_eq!(decoded.name.as_deref(), Some("answer"));
assert!(decoded.parts.is_empty());
let raw = String::from_utf8_lossy(&frame).into_owned();
assert!(
!raw.contains("the answer text"),
"content is not stored: {raw}"
);
assert!(
!raw.contains("the long description"),
"content is not stored: {raw}"
);
}
#[tokio::test]
async fn a_large_payload_is_not_stored_in_the_family() {
let huge = "x".repeat(1024 * 1024);
let frame = message_frame(&message(&huge)).expect("encodes");
assert!(
frame.len() < 256,
"a lifecycle frame is a handful of identifiers: {} bytes",
frame.len()
);
let store = InMemoryTaskStore::new();
let mut opening = task("t1", "c1");
opening.history = Some(vec![message(&huge)]);
store.create(&opening).await.expect("creates");
let stored = store.get("t1").await.expect("reads").expect("exists");
let history = stored.history.expect("the frame is kept");
assert_eq!(
history.len(),
1,
"the message is recorded as having happened"
);
assert_eq!(history[0].message_id, "m1");
assert!(
history[0].parts.is_empty(),
"and its content is not: {:?}",
history[0].parts
);
}
#[tokio::test]
async fn an_oversized_message_id_is_refused_rather_than_recorded() {
let mut long = message("hello");
long.message_id = "m".repeat(MAX_ID_BYTES + 1);
match message_frame(&long) {
Err(TaskStoreError::Refused(reason)) => {
assert!(
reason.contains("message id") && reason.contains(&MAX_ID_BYTES.to_string()),
"the refusal must name the bound: {reason}"
);
}
other => panic!("an oversized message id must be refused, got {other:?}"),
}
let store = InMemoryTaskStore::new();
let mut opening = task("t1", "c1");
opening.history = Some(vec![long]);
assert!(
matches!(
store.create(&opening).await,
Err(TaskStoreError::Refused(_))
),
"and the create is refused with it"
);
assert!(
store.get("t1").await.expect("reads").is_none(),
"nothing is recorded under a refused create"
);
}
#[test]
fn oversized_metadata_is_refused() {
let mut metadata = HashMap::new();
metadata.insert(
"pendingApprovalToolName".to_owned(),
Value::from("x".repeat(MAX_METADATA_BYTES + 1)),
);
assert!(matches!(
metadata_frame(Some(&metadata)),
Err(TaskStoreError::Refused(_))
));
}
#[test]
fn an_empty_status_records_no_detail() {
assert!(
status_detail_frame(&TaskStatus::default())
.expect("encodes")
.is_empty()
);
}
#[test]
fn a_record_rebuilds_the_task_it_stands_for() {
let status = TaskStatus {
state: TaskState::Completed,
message: Some(message("42")),
timestamp: None,
};
let record = AgentTaskRecord {
task_id: "t1".to_owned(),
context_id: "ctx-1".to_owned(),
state: AgentTaskState::Completed,
status_detail: status_detail_frame(&status).expect("encodes"),
artifacts: vec![
artifact_frame(&Artifact {
artifact_id: "a1".to_owned(),
name: None,
description: None,
parts: vec![Part::text("42")],
metadata: None,
})
.expect("encodes"),
],
history: vec![message_frame(&message("hello")).expect("encodes")],
metadata: Vec::new(),
created_at_ms: 1,
updated_at_ms: 2,
};
let rebuilt = task_from_record(&record);
assert_eq!(rebuilt.id, "t1");
assert_eq!(rebuilt.context_id, "ctx-1");
assert_eq!(rebuilt.status.state, TaskState::Completed);
let status_message = rebuilt.status.message.expect("the status names a message");
assert_eq!(status_message.message_id, "m1");
assert!(status_message.parts.is_empty(), "no content is recorded");
assert_eq!(rebuilt.artifacts.as_ref().map(Vec::len), Some(1));
assert_eq!(rebuilt.history.as_ref().map(Vec::len), Some(1));
assert!(rebuilt.metadata.is_none());
}
#[test]
fn a_claim_that_states_no_time_still_reports_when_it_landed() {
let record = AgentTaskRecord {
task_id: "t1".to_owned(),
context_id: "ctx-1".to_owned(),
state: AgentTaskState::Working,
status_detail: Vec::new(),
artifacts: Vec::new(),
history: Vec::new(),
metadata: Vec::new(),
created_at_ms: 1_767_225_600_000,
updated_at_ms: 1_767_225_600_000,
};
let rebuilt = task_from_record(&record);
assert_eq!(rebuilt.status.state, TaskState::Working);
assert_eq!(
rebuilt.status.timestamp.as_deref(),
Some("2026-01-01T00:00:00.000Z"),
"a stranded task must say when it last moved"
);
}
#[test]
fn a_stated_time_wins_over_the_records_own() {
let status = TaskStatus {
state: TaskState::Completed,
message: None,
timestamp: Some("2026-02-02T02:02:02Z".to_owned()),
};
let record = AgentTaskRecord {
task_id: "t1".to_owned(),
context_id: "ctx-1".to_owned(),
state: AgentTaskState::Completed,
status_detail: status_detail_frame(&status).expect("encodes"),
artifacts: Vec::new(),
history: Vec::new(),
metadata: Vec::new(),
created_at_ms: 1,
updated_at_ms: 1_767_225_600_000,
};
assert_eq!(
task_from_record(&record).status.timestamp.as_deref(),
Some("2026-02-02T02:02:02Z")
);
}
#[test]
fn a_record_naming_no_time_reports_none() {
assert_eq!(transition_timestamp(0), None);
}
#[test]
fn an_undecodable_frame_is_dropped_rather_than_failing_the_read() {
let record = AgentTaskRecord {
task_id: "t1".to_owned(),
context_id: "ctx-1".to_owned(),
state: AgentTaskState::Failed,
status_detail: b"not json".to_vec(),
artifacts: Vec::new(),
history: vec![b"not json".to_vec()],
metadata: b"not json".to_vec(),
created_at_ms: 1,
updated_at_ms: 1,
};
let rebuilt = task_from_record(&record);
assert_eq!(rebuilt.status.state, TaskState::Failed);
assert!(rebuilt.status.message.is_none());
assert!(rebuilt.history.is_none());
assert!(rebuilt.metadata.is_none());
}
#[test]
fn dial_failures_keep_refusals_apart_from_outages() {
let of = |code: ErrorCode| from_dial_error(&DialError::Connect(connect_error(code)));
assert_eq!(of(ErrorCode::NotFound), TaskStoreError::NotFound);
assert_eq!(of(ErrorCode::AlreadyExists), TaskStoreError::AlreadyExists);
assert_eq!(of(ErrorCode::FailedPrecondition), TaskStoreError::Terminal);
assert!(matches!(
of(ErrorCode::InvalidArgument),
TaskStoreError::Refused(_)
));
assert!(matches!(
of(ErrorCode::Unavailable),
TaskStoreError::Unavailable(_)
));
assert!(matches!(
of(ErrorCode::Internal),
TaskStoreError::Unavailable(_)
));
}
fn connect_error(code: ErrorCode) -> polyc_rpc_client::ConnectError {
polyc_rpc_client::ConnectError::new(code, "boom")
}
#[tokio::test]
async fn unconfigured_store_fails_naming_the_unset_address() {
let err = UnconfiguredTaskStore
.get("t1")
.await
.expect_err("must fail closed");
match err {
TaskStoreError::Unavailable(message) => assert!(
message.contains("POLYCHROME_AGENT_ADDR"),
"message must name the unset variable: {message}"
),
other => panic!("expected an outage, got {other:?}"),
}
}
#[tokio::test]
async fn the_double_refuses_a_duplicate_create_and_a_terminal_move() {
let store = InMemoryTaskStore::new();
store.create(&task("t1", "c1")).await.expect("creates");
assert_eq!(
store.create(&task("t1", "c1")).await,
Err(TaskStoreError::AlreadyExists)
);
let done = TaskStatus {
state: TaskState::Completed,
message: None,
timestamp: None,
};
store
.transition(
"t1",
TaskUpdate {
status: &done,
artifacts: None,
metadata: None,
appended_history: &[],
},
None,
)
.await
.expect("first move");
assert_eq!(
store
.transition(
"t1",
TaskUpdate {
status: &done,
artifacts: None,
metadata: None,
appended_history: &[],
},
None,
)
.await,
Err(TaskStoreError::Terminal),
"terminal is final for a transition, not only for a cancel"
);
assert_eq!(store.cancel("t1").await, Err(TaskStoreError::Terminal));
assert_eq!(store.cancel("nope").await, Err(TaskStoreError::NotFound));
}
#[tokio::test]
async fn the_double_pages_one_context_by_keyset() {
let store = InMemoryTaskStore::new();
for id in ["t1", "t2", "t3"] {
store.create(&task(id, "c1")).await.expect("creates");
}
store.create(&task("t9", "c2")).await.expect("creates");
let first = store.list("c1", 2, None).await.expect("lists");
assert_eq!(
first
.tasks
.iter()
.map(|t| t.id.as_str())
.collect::<Vec<_>>(),
["t1", "t2"]
);
assert_eq!(first.next_page_token.as_deref(), Some("t2"));
let second = store
.list("c1", 2, first.next_page_token.as_deref())
.await
.expect("lists");
assert_eq!(
second
.tasks
.iter()
.map(|t| t.id.as_str())
.collect::<Vec<_>>(),
["t3"]
);
assert_eq!(second.next_page_token, None);
}
}