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§
Sourcetype Workspace: WorkspaceBackend
type Workspace: WorkspaceBackend
Concrete workspace implementation produced by this agent.
Required Methods§
Sourcefn id(&self) -> AgentId
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);Sourcefn info(&self) -> impl Future<Output = Result<AgentInfo, KitError>> + Send + '_
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());Sourcefn new_workspace(
&self,
request: NewWorkspace,
) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_
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?;Sourcefn list_workspaces(
&self,
) -> impl Future<Output = Result<Vec<WorkspaceInfo>, KitError>> + Send + '_
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());Sourcefn workspace_info(
&self,
id: WorkspaceId,
) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_
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);Sourcefn get_workspace(
&self,
id: WorkspaceId,
) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_
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?;Sourcefn check_workspace_update(
&self,
request: &UpdateWorkspaceRequest,
) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_
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?;Sourcefn update_workspace(
&self,
request: UpdateWorkspaceRequest,
) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_
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);Sourcefn delete_workspace(
&self,
request: DeleteWorkspaceRequest,
) -> impl Future<Output = Result<(), KitError>> + Send + '_
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());Sourcefn list_models(
&self,
) -> impl Future<Output = Result<Vec<ModelInfo>, KitError>> + Send + '_
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
}Sourcefn ingest_knowledge(
&self,
request: IngestKnowledgeRequest,
) -> impl Future<Output = Result<(), KitError>> + Send + '_
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
}Sourcefn shutdown(self) -> impl Future<Output = Result<(), KitError>> + Sendwhere
Self: Sized,
fn shutdown(self) -> impl Future<Output = Result<(), KitError>> + Sendwhere
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".