Skip to main content

TaskStore

Trait TaskStore 

Source
pub trait TaskStore:
    Send
    + Sync
    + 'static {
    // Required methods
    fn save<'a>(
        &'a self,
        task: &'a Task,
    ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>;
    fn get<'a>(
        &'a self,
        id: &'a TaskId,
    ) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>>;
    fn list<'a>(
        &'a self,
        params: &'a ListTasksParams,
    ) -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>>;
    fn insert_if_absent<'a>(
        &'a self,
        task: &'a Task,
    ) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>>;
    fn delete<'a>(
        &'a self,
        id: &'a TaskId,
    ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>;

    // Provided methods
    fn count<'a>(
        &'a self,
    ) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> { ... }
    fn save_artifact_delta<'a>(
        &'a self,
        task: &'a Task,
        delta: ArtifactDelta,
    ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> { ... }
}
Expand description

Trait for persisting and retrieving Task objects.

All methods return Pin<Box<dyn Future>> for object safety — this trait is used as Box<dyn TaskStore>.

§Object safety

Do not add async fn methods; use the explicit Pin<Box<...>> form.

§Example

use std::future::Future;
use std::pin::Pin;
use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::params::ListTasksParams;
use a2a_protocol_types::responses::TaskListResponse;
use a2a_protocol_types::task::{Task, TaskId};
use a2a_protocol_server::store::TaskStore;

/// A no-op store that rejects all operations (for illustration).
struct NullStore;

impl TaskStore for NullStore {
    fn save<'a>(&'a self, _task: &'a Task)
        -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>
    {
        Box::pin(async { Ok(()) })
    }

    fn get<'a>(&'a self, _id: &'a TaskId)
        -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>>
    {
        Box::pin(async { Ok(None) })
    }

    fn list<'a>(&'a self, _params: &'a ListTasksParams)
        -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>>
    {
        Box::pin(async { Ok(TaskListResponse::new(vec![])) })
    }

    fn insert_if_absent<'a>(&'a self, _task: &'a Task)
        -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>>
    {
        Box::pin(async { Ok(true) })
    }

    fn delete<'a>(&'a self, _id: &'a TaskId)
        -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>
    {
        Box::pin(async { Ok(()) })
    }
}

Required Methods§

Source

fn save<'a>( &'a self, task: &'a Task, ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>

Saves (creates or updates) a task.

§Errors

Returns an A2aError if the store operation fails.

Source

fn get<'a>( &'a self, id: &'a TaskId, ) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>>

Retrieves a task by its ID, returning None if not found.

§Errors

Returns an A2aError if the store operation fails.

Source

fn list<'a>( &'a self, params: &'a ListTasksParams, ) -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>>

Lists tasks matching the given filter parameters.

§Errors

Returns an A2aError if the store operation fails.

Source

fn insert_if_absent<'a>( &'a self, task: &'a Task, ) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>>

Atomically inserts a task only if no task with the same ID exists.

Returns Ok(true) if the task was inserted, Ok(false) if a task with the same ID already exists (no modification made).

§Errors

Returns an A2aError if the store operation fails.

Source

fn delete<'a>( &'a self, id: &'a TaskId, ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>

Deletes a task by its ID.

§Errors

Returns an A2aError if the store operation fails.

Provided Methods§

Source

fn count<'a>( &'a self, ) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>>

Returns the total number of tasks in the store.

Useful for monitoring, metrics, and capacity management. Has a default implementation that returns 0 so existing implementations are not broken when this method is added.

§Errors

Returns an A2aError if the store operation fails.

Source

fn save_artifact_delta<'a>( &'a self, task: &'a Task, delta: ArtifactDelta, ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>

Persists an artifact change that has already been applied to task.

§Why this exists

A streaming agent emits one artifact event per chunk, and the obvious implementation persists each one with save — which hands the store the whole task. The task grows with every chunk, so the cost of one event is proportional to the number of events before it, and the cost of a stream is quadratic in its length. Measured on the backpressure/append_volume benchmark, a 502-event stream spent 43.4 ms against the in-memory store versus 3.2 ms against a store that discards everything: 13.5× of that stream was re-persisting artifacts already persisted.

delta says exactly what changed, so a store that can update a record in place does work proportional to the change rather than to the record.

§Implementing this

The default replaces the whole record via save, which is always correct — every existing implementation keeps working unchanged, and a store with no incremental update path should keep it. Overriding is worthwhile for any store where applying a delta is cheaper than rewriting the record.

All three stores shipped here override it, and what each one wins differs with its storage model:

StoreApproachMeasured on a 500-chunk stream
InMemoryTaskStoreMutates the stored task in place43.4 ms to 2.5 ms
SqliteTaskStorejson_set splices the tail into the document144.5 ms to 127.6 ms
PostgresTaskStorejsonb_set with || array concat798 ms to 500 ms

The in-memory win is the largest because a full save there is a deep clone and a delta is a Vec extend. The SQL stores keep one JSON document per row, so they still rewrite the row internally; what the delta removes is the Rust-side serialization of the whole task and its transfer as a bind parameter. That is enough to flatten Postgres’s per-event cost — 874, 1183, 1597 µs at 50, 250 and 500 chunks with save, against 853, 840, 1000 µs with the delta — but not to make either SQL store as cheap as memory. Only normalising artifacts into their own table would do that, and the same measurements put the per-event round trip well above the document-size term, so it would buy the smaller half.

An override must leave the store holding exactly what save(task) would have left it holding. delta describes a change already present in task; if an implementation cannot apply it — the record is missing, or its shape does not match — it must fall back to save(task) rather than persist a divergent record.

§Errors

Returns an A2aError if the store operation fails.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§