Skip to main content

AgentBackend

Trait AgentBackend 

Source
pub trait AgentBackend:
    Send
    + Sync
    + 'static {
    type Workspace: WorkspaceBackend;

    // Required methods
    fn id(&self) -> AgentId;
    fn info(
        &self,
    ) -> impl Future<Output = Result<AgentInfo, KitError>> + Send + '_;
    fn new_workspace(
        &self,
        request: NewWorkspace,
    ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_;
    fn list_workspaces(
        &self,
    ) -> impl Future<Output = Result<Vec<WorkspaceInfo>, KitError>> + Send + '_;
    fn workspace_info(
        &self,
        id: WorkspaceId,
    ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
    fn get_workspace(
        &self,
        id: WorkspaceId,
    ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_;
    fn check_workspace_update(
        &self,
        request: &UpdateWorkspaceRequest,
    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_;
    fn update_workspace(
        &self,
        request: UpdateWorkspaceRequest,
    ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
    fn delete_workspace(
        &self,
        request: DeleteWorkspaceRequest,
    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
    fn list_models(
        &self,
    ) -> impl Future<Output = Result<Vec<ModelInfo>, KitError>> + Send + '_;
    fn ingest_knowledge(
        &self,
        request: IngestKnowledgeRequest,
    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
    fn shutdown(self) -> impl Future<Output = Result<(), KitError>> + Send
       where Self: Sized;
}
Expand description

Public implementation contract for a complete portable agent runtime.

Built-ins and caller implementations use this behavioral boundary directly. A caller implementation returns its own concrete Workspace implementation; runtime composition can instead use a caller-owned exhaustive enum hierarchy. Accepted work must reach its documented durable terminal state even if a caller drops the awaiting future. Shutdown seals new work, invalidates live descendants, and settles accepted cleanup.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

use agents_kit::identity::AgentInfo;
use agents_kit::KitError;
async fn inspect<A: AgentBackend>(agent: &A) -> Result<AgentInfo, KitError> {
    let info = agent.info().await?;
    assert_eq!(info.id(), &agent.id());
    Ok(info)
}
assert_eq!(inspect(&agent).await?.id(), &agent.id());

§Caller-owned concrete composition

This is an illustrative application enum hierarchy, not a new durable engine or evidence of native capability. It supports one supplied family here and delegates every operation, including errors and consuming cleanup, to its real owner. Add another concrete family with matching child enum arms when your application needs it. There is no Agent::from_backend ingress, no erased backend, and no independent Workspace/Conversation type parameter.

The example executes local creation, metadata access and cleanup. The supplied Custom implementation retains responsibility for persistence, revision fencing, notifications, prompt admission and shutdown. It performs no model request.

use std::{future::Future, num::NonZeroUsize, path::PathBuf, time::Duration};
use agents_kit::{Agent, AgentBackend, AgentBuilder, AgentStorage, Conversation,
    ConversationBackend, KitError, Workspace, WorkspaceBackend};
use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
use agents_kit::conversation::{ConversationInfo, FileRead, NewConversation};
use agents_kit::identity::{AgentId, AgentInfo, ConversationId, NotificationId, WorkspaceId};
use agents_kit::knowledge::IngestKnowledgeRequest;
use agents_kit::model::{ModelInfo, ModelSelector};
use agents_kit::notification::{ConversationNotificationReceiver, NotificationResolution};
use agents_kit::prompt::{PromptRequest, PromptResponse, PromptStream};
use agents_kit::tool::{ToolInfo, ToolSelection};
use agents_kit::workspace::{DeleteWorkspaceRequest, NewWorkspace, UpdateWorkspaceRequest,
    WorkspaceContext, WorkspaceDefaults, WorkspaceExecutionPolicy, WorkspaceInfo,
    WorkspaceNetworkAccess, WorkspaceRoot, WorkspaceRootAccess, WorkspaceSandboxMode,
    WorkspaceUpdateReadiness};
use providers_kit::{Provider, ProviderConfig,
    config::{OllamaConfig, ProviderKind, TransportConfig}};
use storage_kit::{StateStore,
    state::config::{LanceStateStoreConfig, StateStoreConfig}};

enum ApplicationAgent { Supplied(Agent) }
enum ApplicationWorkspace { Supplied(Workspace) }
enum ApplicationConversation { Supplied(Conversation) }

impl AgentBackend for ApplicationAgent {
    type Workspace = ApplicationWorkspace;
    fn id(&self) -> AgentId {
        match self { Self::Supplied(value) => value.id() }
    }
    async fn info(&self) -> Result<AgentInfo, KitError> {
        match self { Self::Supplied(value) => value.info().await }
    }
    async fn new_workspace(&self, request: NewWorkspace) -> Result<Self::Workspace, KitError> {
        match self { Self::Supplied(value) => value.new_workspace(request).await.map(ApplicationWorkspace::Supplied) }
    }
    async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, KitError> {
        match self { Self::Supplied(value) => value.list_workspaces().await }
    }
    async fn workspace_info(&self, id: WorkspaceId) -> Result<WorkspaceInfo, KitError> {
        match self { Self::Supplied(value) => value.workspace_info(id).await }
    }
    async fn get_workspace(&self, id: WorkspaceId) -> Result<Self::Workspace, KitError> {
        match self { Self::Supplied(value) => value.get_workspace(id).await.map(ApplicationWorkspace::Supplied) }
    }
    fn check_workspace_update(&self, request: &UpdateWorkspaceRequest)
        -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_ {
        // Construct the delegated future now; it does not borrow the request.
        match self { Self::Supplied(value) => value.check_workspace_update(request) }
    }
    async fn update_workspace(&self, request: UpdateWorkspaceRequest) -> Result<WorkspaceInfo, KitError> {
        match self { Self::Supplied(value) => value.update_workspace(request).await }
    }
    async fn delete_workspace(&self, request: DeleteWorkspaceRequest) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.delete_workspace(request).await }
    }
    async fn list_models(&self) -> Result<Vec<ModelInfo>, KitError> {
        match self { Self::Supplied(value) => value.list_models().await }
    }
    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.ingest_knowledge(request).await }
    }
    async fn shutdown(self) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.shutdown().await }
    }
}
impl WorkspaceBackend for ApplicationWorkspace {
    type Conversation = ApplicationConversation;
    fn id(&self) -> WorkspaceId {
        match self { Self::Supplied(value) => value.id() }
    }
    async fn info(&self) -> Result<WorkspaceInfo, KitError> {
        match self { Self::Supplied(value) => value.info().await }
    }
    async fn new_conversation(&self, request: NewConversation) -> Result<Self::Conversation, KitError> {
        match self { Self::Supplied(value) => value.new_conversation(request).await.map(ApplicationConversation::Supplied) }
    }
    async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
        match self { Self::Supplied(value) => value.list_conversations().await }
    }
    async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
        match self { Self::Supplied(value) => value.conversation_info(id).await }
    }
    async fn get_conversation(&self, id: ConversationId) -> Result<Self::Conversation, KitError> {
        match self { Self::Supplied(value) => value.get_conversation(id).await.map(ApplicationConversation::Supplied) }
    }
    async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
        match self { Self::Supplied(value) => value.list_tools().await }
    }
    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.ingest_knowledge(request).await }
    }
    async fn close(self) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.close().await }
    }
}
impl ConversationBackend for ApplicationConversation {
    fn id(&self) -> ConversationId {
        match self { Self::Supplied(value) => value.id() }
    }
    async fn info(&self) -> Result<ConversationInfo, KitError> {
        match self { Self::Supplied(value) => value.info().await }
    }
    async fn prompt(&self, request: PromptRequest) -> Result<PromptResponse, KitError> {
        match self { Self::Supplied(value) => value.prompt(request).await }
    }
    async fn prompt_stream(&self, request: PromptRequest) -> Result<PromptStream, KitError> {
        match self { Self::Supplied(value) => value.prompt_stream(request).await }
    }
    async fn abort(&self) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.abort().await }
    }
    async fn read_file(&self, path: PathBuf) -> Result<FileRead, KitError> {
        match self { Self::Supplied(value) => value.read_file(path).await }
    }
    fn notification_stream(&self) -> Result<ConversationNotificationReceiver, KitError> {
        match self { Self::Supplied(value) => value.notification_stream() }
    }
    async fn resolve_notification(&self, id: NotificationId, resolution: NotificationResolution) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.resolve_notification(id, resolution).await }
    }
    async fn close(self) -> Result<(), KitError> {
        match self { Self::Supplied(value) => value.close().await }
    }
}

let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
let config = AgentConfig::new(CustomAgentConfig::new(ModelSelector::new("exact-model")?,
    ToolSelection::None, CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?.into(),
    Duration::from_secs(10), NonZeroUsize::MIN)?;
let provider = Provider::from_config(ProviderConfig::new(
    ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
let agent = ApplicationAgent::Supplied(AgentBuilder::create(config, AgentStorage::new(store))
    .with_provider(provider).build().await?);
let outcome = async {
    let cwd = std::env::current_dir()?;
    let context = WorkspaceContext::new(cwd.clone(),
        vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
        WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
    let workspace = agent.new_workspace(NewWorkspace::new("caller", context, WorkspaceDefaults::empty())?).await?;
    let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, vec![])?).await?;
    assert_eq!(conversation.info().await?.workspace_id(), &workspace.id());
    assert_eq!(workspace.list_conversations().await?.len(), 1);
    conversation.close().await?;
    workspace.close().await?;
    Ok::<(), Box<dyn std::error::Error>>(())
}.await;
let shutdown = agent.shutdown().await;
outcome?;
shutdown?;

Required Associated Types§

Source

type Workspace: WorkspaceBackend

Concrete workspace implementation produced by this agent.

Required Methods§

Source

fn id(&self) -> AgentId

Returns the durable portable agent identity.

The identity remains stable across runtime reopen. This value identifies the durable Agent; it does not expose backend routing or lower resources.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

let identity = agent.id();
assert_eq!(agent.info().await?.id(), &identity);
Source

fn info(&self) -> impl Future<Output = Result<AgentInfo, KitError>> + Send + '_

Loads the current descriptive agent information.

§Errors

Returns KitError if the runtime is closed, committed state cannot be read, or stored facts contradict the portable record contract.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

let info = agent.info().await?;
assert_eq!(info.id(), &agent.id());
Source

fn new_workspace( &self, request: NewWorkspace, ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_

Creates and publishes a workspace from validated portable configuration.

Publishes a handle only after complete context/defaults, backend attachment, and durable ownership agree. A new workspace begins at revision one.

§Errors

Returns KitError for closed or busy admission, unsupported execution policy/integration composition, invalid input, dependency failure, or deadline expiry. Accepted native setup must remain durably attached or recovery-owned when an operation fails.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

let workspace = agent.new_workspace(
    NewWorkspace::new("review", context, WorkspaceDefaults::empty())?,
).await?;
assert_eq!(workspace.info().await?.revision(), 1);
workspace.close().await?;
Source

fn list_workspaces( &self, ) -> impl Future<Output = Result<Vec<WorkspaceInfo>, KitError>> + Send + '_

Lists the agent’s durable workspaces.

Inventory comes from committed records, not only currently opened handles. The result does not claim live ownership of those workspaces.

§Errors

Returns KitError for a closed runtime, state-read failure, deadline expiry, or inconsistent durable inventory.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

let inventory = agent.list_workspaces().await?;
assert_eq!(inventory.len(), 1);
assert_eq!(inventory[0].id(), &workspace.id());
Source

fn workspace_info( &self, id: WorkspaceId, ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_

Loads descriptive information for a workspace identity.

This reads committed metadata without claiming a live workspace handle. Missing and foreign identities are both reported without revealing owners.

§Errors

Returns crate::error::ErrorKind::NotFound for a missing or foreign id, or the owning lifecycle, state-read, integrity, or deadline error.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

let info = agent.workspace_info(workspace.id()).await?;
assert_eq!(info.name(), "review");
assert_eq!(info.revision(), 1);
Source

fn get_workspace( &self, id: WorkspaceId, ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_

Reopens a durable workspace as a live handle.

At most one live handle exists for a current workspace revision. Share it by reference; a second current-revision lookup is busy. Reopen verifies durable attachment and integration compatibility rather than inventing a replacement workspace.

§Errors

Returns KitError for a missing/foreign id, an existing live claim, closed runtime, incompatible reopen state, dependency failure, or timeout.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

let id = workspace.id();
workspace.close().await?;
let reopened = agent.get_workspace(id.clone()).await?;
assert_eq!(reopened.id(), id);
reopened.close().await?;
Source

fn check_workspace_update( &self, request: &UpdateWorkspaceRequest, ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_

Performs a read-only advisory preflight for a workspace replacement.

A completed Ready reserves no later mutation right. The durable expected revision is checked before dynamic readiness; the preflight excludes its own transient admission from the activity it reports.

§Errors

Returns KitError for invalid or unsupported replacement data, a missing/foreign workspace, stale revision, closed runtime, or an owning state/dependency/deadline failure. Dynamic activity is reported as WorkspaceUpdateReadiness::Busy.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceUpdateReadiness};
let current = workspace.info().await?;
let request = UpdateWorkspaceRequest::new(current.id().clone(), current.revision(),
    "renamed", current.context().clone(), current.defaults().clone())?;
assert_eq!(agent.check_workspace_update(&request).await?, WorkspaceUpdateReadiness::Ready);
// Advisory readiness does not change or reserve the durable revision.
assert_eq!(agent.workspace_info(current.id().clone()).await?.revision(), current.revision());
workspace.close().await?;
Source

fn update_workspace( &self, request: UpdateWorkspaceRequest, ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_

Atomically applies a revision-checked portable workspace replacement.

This is a full replacement of name, context, and defaults, not a patch. The operation rechecks revision and activity atomically, preserves the backend attachment, increments the revision once, and invalidates prior workspace/conversation handles only after durable commit. Commit is success; postcommit cleanup cannot change it into an error.

§Errors

Returns KitError for stale revision, busy or ambiguous descendant work, missing/foreign identity, unsupported replacement policy, closed runtime, or precommit dependency/deadline failures. Revision overflow is crate::error::ErrorKind::InvalidState and does not change state.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

use agents_kit::workspace::UpdateWorkspaceRequest;
let current = workspace.info().await?;
workspace.close().await?;
let replacement = UpdateWorkspaceRequest::new(current.id().clone(), current.revision(),
    "renamed", current.context().clone(), current.defaults().clone())?;
let updated = agent.update_workspace(replacement).await?;
assert_eq!(updated.name(), "renamed");
assert_eq!(updated.revision(), current.revision() + 1);
Source

fn delete_workspace( &self, request: DeleteWorkspaceRequest, ) -> impl Future<Output = Result<(), KitError>> + Send + '_

Deletes a workspace after validating its expected revision.

Requires quiescent descendants and the current revision; there is no force mode. Durable removal of workspace/child inventory is the success boundary. Postcommit native cleanup remains a recovery obligation and cannot reverse success.

§Errors

Returns KitError for stale revision, active or ambiguous descendants, missing/foreign identity, closed runtime, or precommit state/dependency/ deadline failure.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

use agents_kit::workspace::DeleteWorkspaceRequest;
let current = workspace.info().await?;
workspace.close().await?;
agent.delete_workspace(DeleteWorkspaceRequest::new(current.id().clone(), current.revision())?).await?;
assert!(agent.list_workspaces().await?.is_empty());
Source

fn list_models( &self, ) -> impl Future<Output = Result<Vec<ModelInfo>, KitError>> + Send + '_

Lists selectable models in stable canonical-selector order.

The complete selectable inventory retains canonical selectors and supported reasoning/personality values. Listing does not reserve future availability; selection is revalidated in the eventual workspace/conversation context.

§Errors

Returns KitError when the inventory route is unsupported or incomplete, selected external data is malformed, the runtime is closed, or a dependency operation fails or times out. An unavailable inventory is not an empty success.

§Examples

This example requires a live Agent with its configured backend and storage integrations; it is compiled without starting those services.

use agents_kit::{AgentBackend, KitError};
use agents_kit::model::ModelInfo;

async fn available_models<A: AgentBackend>(agent: &A) -> Result<Vec<ModelInfo>, KitError> {
    agent.list_models().await
}
Source

fn ingest_knowledge( &self, request: IngestKnowledgeRequest, ) -> impl Future<Output = Result<(), KitError>> + Send + '_

Replaces the agent-scoped managed-knowledge source set.

An omitted scope selects Agent scope; an explicit Database scope follows the managed-knowledge contract. Nonempty ingestion replaces the selected source set atomically; an empty request is a no-op after scope validation, not deletion. It does not execute a prompt or create tools. See crate::knowledge for a complete local ingestion workflow.

§Errors

Returns KitError for invalid source/scope data, missing or incompatible knowledge integrations, closed or busy admission, dependency failures, or timeout. Unsupported scope or backend semantics fail explicitly.

§Examples

This example requires a live Agent with its configured backend and storage integrations; it is compiled without starting those services.

use agents_kit::{AgentBackend, KitError};
use agents_kit::knowledge::IngestKnowledgeRequest;

async fn replace_sources<A: AgentBackend>(agent: &A, request: IngestKnowledgeRequest)
    -> Result<(), KitError>
{
    agent.ingest_knowledge(request).await
}
Source

fn shutdown(self) -> impl Future<Output = Result<(), KitError>> + Send
where Self: Sized,

Consumes the implementation, seals new work, and settles owned cleanup.

Live descendants are invalidated after accepted work and cleanup settle. Quiescent durable workspaces and conversations remain available to a new parent runtime. Dropping an Agent provides no asynchronous cleanup guarantee.

§Errors

Returns KitError if consuming cleanup cannot fully settle within the lifecycle deadline or an owned resource fails cleanup. The owner is not restored; preserve the primary error and its supplementary cleanup facts and use durable state for later reconciliation.

§Examples

This local Custom agent uses in-memory Lance and sends no network requests.

assert!(agent.list_workspaces().await?.is_empty());
// Consuming shutdown joins owned cleanup before this runtime is dropped.
agent.shutdown().await?;

Dyn Compatibility§

This trait is not dyn compatible.

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

Implementors§

Source§

impl<S, D, P, T> AgentBackend for Agent<S, D, P, T>

Source§

type Workspace = Workspace<S, D, P, T>