pub trait ConversationBackend:
Send
+ Sync
+ 'static {
// Required methods
fn id(&self) -> ConversationId;
fn info(
&self,
) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_;
fn prompt(
&self,
request: PromptRequest,
) -> impl Future<Output = Result<PromptResponse, KitError>> + Send + '_;
fn abort(&self) -> impl Future<Output = Result<(), KitError>> + Send + '_;
fn read_file(
&self,
path: PathBuf,
) -> impl Future<Output = Result<FileRead, KitError>> + Send + '_;
fn notification_stream(
&self,
) -> Result<ConversationNotificationReceiver, KitError>;
fn resolve_notification(
&self,
id: NotificationId,
resolution: NotificationResolution,
) -> impl Future<Output = Result<(), KitError>> + Send + '_;
fn close(self) -> impl Future<Output = Result<(), KitError>> + Send
where Self: Sized;
// Provided method
fn prompt_stream(
&self,
_request: PromptRequest,
) -> impl Future<Output = Result<PromptStream, KitError>> + Send + '_ { ... }
}Expand description
Public implementation contract for one live portable conversation.
A caller workspace returns its own concrete conversation through
WorkspaceBackend::Conversation.
Prompting reaches a terminal portable result, abort joins active work, and close
permanently closes the durable conversation. Runtime composition may use an
exhaustive caller-owned enum hierarchy.
§Runnable caller example
This small test double accepts one prompt and one file path. It demonstrates portable consumption without a service or database; it is not a durable agent. Notification and interaction methods return explicit Unsupported errors; request assertions make the fixture’s expected inputs visible. The streaming response is caller-authored, not fabricated deltas from a supplied backend.
use std::path::{Path, PathBuf};
use agents_kit::{ConversationBackend, KitError};
use agents_kit::conversation::{ConversationInfo, FileRead};
use agents_kit::identity::{ConversationId, NotificationId, WorkspaceId};
use agents_kit::notification::{ConversationNotificationReceiver, NotificationResolution};
use agents_kit::prompt::{PromptEvent, PromptRequest, PromptResponse, PromptStream};
struct ScriptedConversation {
id: ConversationId,
workspace: WorkspaceId,
}
impl ConversationBackend for ScriptedConversation {
fn id(&self) -> ConversationId { self.id.clone() }
async fn info(&self) -> Result<ConversationInfo, KitError> {
ConversationInfo::new(self.id(), self.workspace.clone(), Some("Example".into()))
}
async fn prompt(&self, request: PromptRequest) -> Result<PromptResponse, KitError> {
assert_eq!(request.text(), "Status?");
Ok(PromptResponse::new("ready".into(), None))
}
async fn prompt_stream(&self, request: PromptRequest) -> Result<PromptStream, KitError> {
assert_eq!(request.text(), "Status?");
Ok(PromptStream::from_stream(futures_util::stream::iter([
Ok(PromptEvent::OutputDelta { text: "ready".into() }),
Ok(PromptEvent::Completed(PromptResponse::new("ready".into(), None))),
])))
}
async fn read_file(&self, path: PathBuf) -> Result<FileRead, KitError> {
assert_eq!(path, Path::new("status.txt"));
FileRead::from_bytes(path, b"ready\n".to_vec())
}
// No background work or durable interaction exists in this fixture.
async fn abort(&self) -> Result<(), KitError> { Ok(()) }
fn notification_stream(&self) -> Result<ConversationNotificationReceiver, KitError> {
Err(KitError::unsupported("this fixture has no notifications"))
}
async fn resolve_notification(&self, _: NotificationId, _: NotificationResolution)
-> Result<(), KitError> {
Err(KitError::unsupported("this fixture has no interactions"))
}
async fn close(self) -> Result<(), KitError> { Ok(()) }
}
let conversation = ScriptedConversation {
id: "conversation_00000000000000000000000000000001".parse()?,
workspace: "workspace_00000000000000000000000000000001".parse()?,
};
let outcome = async {
assert_eq!(conversation.info().await?.id(), &conversation.id());
assert_eq!(conversation.prompt(PromptRequest::new("Status?")?).await?.output(), "ready");
let mut output = conversation.prompt_stream(PromptRequest::new("Status?")?).await?;
assert!(matches!(output.recv().await.transpose()?,
Some(PromptEvent::OutputDelta { text }) if text == "ready"));
assert!(matches!(output.recv().await.transpose()?,
Some(PromptEvent::Completed(response)) if response.output() == "ready"));
assert!(output.recv().await.is_none());
assert!(output.recv().await.is_none());
assert_eq!(conversation.read_file("status.txt".into()).await?.text(), Some("ready\n"));
conversation.abort().await?;
Ok::<(), KitError>(())
}.await;
let cleanup = conversation.close().await;
match (outcome, cleanup) {
(Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
(Err(error), _) | (_, Err(error)) => Err(error),
(Ok(()), Ok(())) => Ok(()),
}Required Methods§
Sourcefn id(&self) -> ConversationId
fn id(&self) -> ConversationId
Returns the durable portable conversation identity.
See the runnable caller implementation on ConversationBackend. Supplied backends retain their own runtime and storage prerequisites.
Sourcefn info(
&self,
) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_
fn info( &self, ) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_
Reads the current descriptive conversation state.
§Errors
Returns Closed for a terminal handle, Conflict for stale ownership/revision, or a typed storage/backend failure.
See the runnable caller implementation on ConversationBackend. Supplied backends retain their own runtime and storage prerequisites.
Sourcefn prompt(
&self,
request: PromptRequest,
) -> impl Future<Output = Result<PromptResponse, KitError>> + Send + '_
fn prompt( &self, request: PromptRequest, ) -> impl Future<Output = Result<PromptResponse, KitError>> + Send + '_
Runs one foreground prompt through its terminal portable result.
Unary and incremental calls share one accepted prompt owner. Dropping the waiter does not cancel accepted work or free its slot. Success retains exact authored text; structured output is separately validated and usage is present only when sourceable.
§Errors
Returns InvalidInput/Unsupported before acceptance for invalid or unsupported requests, Busy when admission is occupied, or typed cancellation, deadline, conflict, storage, and backend failures. After ambiguous acceptance, do not blindly retry: cancellation/reconciliation retains the generation fence until settlement.
See the runnable caller implementation on ConversationBackend. Supplied backends retain their own runtime and storage prerequisites.
Sourcefn abort(&self) -> impl Future<Output = Result<(), KitError>> + Send + '_
fn abort(&self) -> impl Future<Output = Result<(), KitError>> + Send + '_
Aborts the active prompt, or succeeds when no prompt is active.
Cancellation targets only the active generation and joins its settlement. Natural completion may win; abort does not replace the prompt waiter or return its answer.
§Errors
Returns a typed cancellation, timeout, conflict, lifecycle, or backend failure. Ambiguous cancellation retains recovery ownership; an error does not prove remote work stopped.
See the runnable caller implementation on ConversationBackend. Supplied backends retain their own runtime and storage prerequisites.
Sourcefn read_file(
&self,
path: PathBuf,
) -> impl Future<Output = Result<FileRead, KitError>> + Send + '_
fn read_file( &self, path: PathBuf, ) -> impl Future<Output = Result<FileRead, KitError>> + Send + '_
Reads a validated workspace-relative file through the implementation.
The path is interpreted against the admitted workspace cwd on the backend host. Absolute paths, dot/parent components and repeated separators are rejected rather than normalized.
§Errors
Returns InvalidInput for a nonportable relative path, Unsupported for a route unable to preserve exact bytes/policy, or a typed revision, lifecycle, storage, or native read failure. An untyped native rejection is not evidence of NotFound.
See the runnable caller implementation on ConversationBackend. Supplied backends retain their own runtime and storage prerequisites.
Sourcefn notification_stream(
&self,
) -> Result<ConversationNotificationReceiver, KitError>
fn notification_stream( &self, ) -> Result<ConversationNotificationReceiver, KitError>
Claims the conversation’s exclusive durable notification receiver.
§Errors
Returns Busy while another receiver owns the lease, Closed for a closed handle, or a typed admission failure. Later durable delivery failures are yielded by recv.
See ConversationNotificationReceiver for executable caller-stream delivery, and the native interaction recipe for durable delivery.
Sourcefn resolve_notification(
&self,
id: NotificationId,
resolution: NotificationResolution,
) -> impl Future<Output = Result<(), KitError>> + Send + '_
fn resolve_notification( &self, id: NotificationId, resolution: NotificationResolution, ) -> impl Future<Output = Result<(), KitError>> + Send + '_
Submits a resolution for an unresolved portable notification.
§Errors
Returns InvalidInput for an invalid resolution, NotFound for an unknown notification, or typed lifecycle, conflict, storage, and native resolution failures. Intent is durable before dispatch; known or ambiguous acceptance is never blindly resent.
See NotificationResolution for resolution values and the native interaction recipe for resolving an admitted interaction. Caller implementations are exercised by the direct hierarchy tests without a service.
Sourcefn close(self) -> impl Future<Output = Result<(), KitError>> + Sendwhere
Self: Sized,
fn close(self) -> impl Future<Output = Result<(), KitError>> + Sendwhere
Self: Sized,
Permanently closes the conversation and consumes the implementation.
Unlike parent workspace close or Agent shutdown, this permanently closes the durable conversation. Admission is sealed and active work, interactions, knowledge and owned resources are settled before completion.
§Errors
Returns a typed cancellation, reconciliation, storage, or cleanup failure. The handle remains consumed on failure; durable recovery obligations remain authoritative.
See the runnable caller implementation on ConversationBackend. Supplied backends retain their own runtime and storage prerequisites.
Provided Methods§
Sourcefn prompt_stream(
&self,
_request: PromptRequest,
) -> impl Future<Output = Result<PromptStream, KitError>> + Send + '_
fn prompt_stream( &self, _request: PromptRequest, ) -> impl Future<Output = Result<PromptStream, KitError>> + Send + '_
Starts real incremental observation of one foreground prompt.
The default rejects unsupported streaming before accepting work. Override
using public PromptStream and PromptEvent
values; do not fabricate deltas from a terminal response. Dropping an
observer does not cancel accepted work or release the conversation slot.
§Errors
Returns Unsupported when the backend has no genuine incremental route, or the same admission failures as prompt. After admission, the stream carries terminal failures; premature source EOF is a protocol failure.
The runnable ConversationBackend example consumes real caller-stream events. PromptStream also demonstrates terminal validation; the native interaction recipe requires a configured service.
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".