Skip to main content

ConversationBackend

Trait ConversationBackend 

Source
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§

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

fn close(self) -> impl Future<Output = Result<(), KitError>> + Send
where 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§

Source

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".

Implementors§