agents_kit/conversation.rs
1//! Conversation metadata, accepted prompts, exact file reads and durable interactions.
2//!
3//! Creation inputs are passive. Live operations use [`ConversationBackend`]; its
4//! consuming close permanently terminates the conversation. Prompt streams observe
5//! runtime-owned accepted work, so dropping an observer is not cancellation.
6//!
7//! # Streaming, durable approval and explicit cancellation
8//!
9//! This complete Custom journey requires a running Ollama server and the exact
10//! configured model with function-calling support. It is therefore `no_run`.
11//! Replace only the caller configuration to use your intended model; selection
12//! does not substitute a fallback. The caller supplies the executable tool.
13//! Caller-built actions run in process without Kit sandbox enforcement, so Custom
14//! requires `FullAccess`, `Enabled` and read-write roots for this configuration; roots do
15//! not confine the action. This example's `inspect_status` action performs no
16//! filesystem or network I/O.
17//!
18//! Notifications are an exclusive, durable, at-least-once delivery path. Inspect
19//! an Interaction before resolving its exact ID; advancing passive delivery is
20//! acknowledgment, and dropping the receiver is not acknowledgment. The example
21//! deliberately denies every requested action: a production UI can instead
22//! collect a caller decision and send `Allow` or schema-valid `Submit` content.
23//! Denial is a real interaction resolution, not a fabricated tool result.
24//!
25//! The output observer requires the authoritative `Completed` value. Deltas can
26//! include earlier assistant turns around tool execution, so concatenating them
27//! is not an alternative final answer. Observer loss does not abort accepted work;
28//! the separate cancellation portion uses `abort`, then observes settlement.
29//!
30//! ```no_run
31//! use std::{num::NonZeroUsize, time::Duration};
32//! use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, ConversationBackend, WorkspaceBackend};
33//! use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
34//! use agents_kit::conversation::NewConversation;
35//! use agents_kit::notification::{ConversationNotificationKind, NotificationResolution};
36//! use agents_kit::prompt::{PromptEvent, PromptRequest};
37//! use agents_kit::{model::ModelSelector, tool::ToolSelection};
38//! use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
39//! WorkspaceExecutionPolicy, WorkspaceNetworkAccess, WorkspaceRoot,
40//! WorkspaceRootAccess, WorkspaceSandboxMode};
41//! use providers_kit::{Provider, ProviderConfig,
42//! config::{OllamaConfig, ProviderKind, TransportConfig}};
43//! use storage_kit::{StateStore,
44//! state::config::{LanceStateStoreConfig, StateStoreConfig}};
45//! use tools_kit::{Toolset, declaration::{ToolInfo, ToolName}, execution::ToolResult,
46//! local::ToolRegistration, schema::JsonSchema};
47//! # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
48//! # runtime.block_on(async {
49//! let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
50//! let tools = Toolset::local(vec![ToolRegistration::function(
51//! ToolInfo::new(ToolName::new("inspect_status")?,
52//! JsonSchema::strict(serde_json::json!({"type": "object", "additionalProperties": false}))),
53//! |_| async { Ok(ToolResult::json(serde_json::json!({"status": "ready"}))) },
54//! )])?;
55//! let custom = CustomAgentConfig::new(ModelSelector::new("exact-tool-capable-model")?,
56//! ToolSelection::Auto, CustomLoopLimits::new(4, 4, 1)?, CustomToolApproval::Interaction)?;
57//! let config = AgentConfig::new(custom.into(), Duration::from_secs(60), NonZeroUsize::MIN)?;
58//! let provider = Provider::from_config(ProviderConfig::new(
59//! ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
60//! let agent = AgentBuilder::create(config, AgentStorage::new(store))
61//! .with_provider(provider).with_toolset(tools).build().await?;
62//! let outcome = async {
63//! let cwd = std::env::current_dir()?;
64//! let context = WorkspaceContext::new(cwd.clone(),
65//! vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadWrite)?],
66//! WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::FullAccess, WorkspaceNetworkAccess::Enabled))?;
67//! let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
68//! let conversation = workspace.new_conversation(NewConversation::new(None, None, vec![])?).await?;
69//! let work = async {
70//! let mut notifications = conversation.notification_stream()?;
71//! let request = PromptRequest::new("Call inspect_status, then describe its outcome.")?
72//! .with_tool_selection(ToolSelection::Required)?
73//! .with_timeout(Duration::from_secs(90))?;
74//! let mut output = conversation.prompt_stream(request).await?;
75//! let completion = tokio::time::timeout(Duration::from_secs(90), async {
76//! loop {
77//! tokio::select! {
78//! event = output.recv() => match event {
79//! Some(Ok(PromptEvent::OutputDelta { text })) => print!("{text}"),
80//! Some(Ok(PromptEvent::Completed(response))) => break Ok(response),
81//! Some(Err(error)) => break Err(error),
82//! None => break Err(agents_kit::KitError::protocol("completion is missing")),
83//! },
84//! event = notifications.recv() => {
85//! let event = event.ok_or_else(|| agents_kit::KitError::closed("notification stream ended"))??;
86//! match event.kind() {
87//! ConversationNotificationKind::Interaction { title, body, .. } => {
88//! println!("Decision requested: {title}: {body}");
89//! conversation.resolve_notification(event.id().clone(),
90//! NotificationResolution::Deny { reason: "No action approved by this example".into() }).await?;
91//! }
92//! _ => println!("Observed passive notification {}", event.id()),
93//! }
94//! }
95//! }
96//! }
97//! }).await.map_err(|_| agents_kit::KitError::timeout("caller observation deadline"))??;
98//! assert!(output.recv().await.is_none());
99//! assert!(!completion.output().trim().is_empty());
100//! println!("Authoritative answer: {}", completion.output());
101//!
102//! // Admission succeeds before cancellation. Dropping `output` would not cancel.
103//! let mut cancelled = conversation.prompt_stream(PromptRequest::new("Explain the task in detail.")?
104//! .with_tool_selection(ToolSelection::None)?
105//! .with_timeout(Duration::from_secs(90))?).await?;
106//! conversation.abort().await?;
107//! let terminal = tokio::time::timeout(Duration::from_secs(90), async {
108//! loop {
109//! match cancelled.recv().await {
110//! Some(Ok(PromptEvent::OutputDelta { .. })) => {}
111//! Some(Ok(PromptEvent::Completed(_))) => break Ok(()), // completion may win the race
112//! Some(Err(error)) if error.kind() == agents_kit::error::ErrorKind::Cancelled => break Ok(()),
113//! Some(Err(error)) => break Err(error),
114//! None => break Err(agents_kit::KitError::protocol("cancellation terminal is missing")),
115//! }
116//! }
117//! }).await.map_err(|_| agents_kit::KitError::timeout("cancellation observation deadline"))?;
118//! terminal?;
119//! Ok::<(), agents_kit::KitError>(())
120//! }.await;
121//! let close = conversation.close().await; // also settles work after observation failure
122//! let workspace_close = workspace.close().await;
123//! work?;
124//! close?;
125//! workspace_close?;
126//! Ok::<(), Box<dyn std::error::Error>>(())
127//! }.await;
128//! let shutdown = agent.shutdown().await;
129//! outcome?;
130//! shutdown?;
131//! # Ok::<(), Box<dyn std::error::Error>>(())
132//! # })?;
133//! # Ok::<(), Box<dyn std::error::Error>>(())
134//! ```
135//!
136use std::future::Future;
137use std::path::PathBuf;
138use tools_kit::ToolsetBackend;
139
140use providers_kit::ProviderBackend;
141use storage_kit::{DatabaseBackend, StateStoreBackend};
142
143pub use crate::file::FileRead;
144
145use crate::backends::{codex, custom, opencode};
146use crate::prompt::PromptStream;
147use crate::{
148 ConversationId, ConversationNotificationReceiver, KitError, KnowledgeSource, NotificationId,
149 NotificationResolution, PromptRequest, PromptResponse, WorkspaceDefaults, WorkspaceId,
150};
151
152/// Descriptive state for one portable conversation.
153///
154/// # Examples
155///
156/// ```
157/// use agents_kit::conversation::ConversationInfo;
158/// let id = "conversation_00000000000000000000000000000000".parse()?;
159/// let workspace = "workspace_00000000000000000000000000000000".parse()?;
160/// let info = ConversationInfo::new(id, workspace, Some("Review".into()))?;
161/// assert_eq!(info.title(), Some("Review"));
162/// # Ok::<(), agents_kit::KitError>(())
163/// ```
164#[derive(Clone, Debug, Eq, PartialEq)]
165pub struct ConversationInfo {
166 id: ConversationId,
167 workspace_id: WorkspaceId,
168 title: Option<String>,
169}
170
171impl ConversationInfo {
172 /// Validates and constructs descriptive conversation information.
173 ///
174 /// Titles, when present, must be nonempty and must not contain leading or
175 /// trailing whitespace.
176 ///
177 /// # Errors
178 ///
179 /// Returns [`crate::error::ErrorKind::InvalidInput`] when `title` violates the portable
180 /// naming contract.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// use agents_kit::{
186 /// conversation::ConversationInfo,
187 /// identity::{ConversationId, WorkspaceId},
188 /// };
189 ///
190 /// let id: ConversationId =
191 /// "conversation_00000000000000000000000000000000".parse()?;
192 /// let workspace_id: WorkspaceId =
193 /// "workspace_00000000000000000000000000000000".parse()?;
194 /// let info =
195 /// ConversationInfo::new(id, workspace_id, Some("release".to_owned()))?;
196 /// assert_eq!(info.title(), Some("release"));
197 /// # Ok::<(), agents_kit::KitError>(())
198 /// ```
199 pub fn new(
200 id: ConversationId,
201 workspace_id: WorkspaceId,
202 title: Option<String>,
203 ) -> Result<Self, KitError> {
204 Self::validate_title(title.as_deref())?;
205 Ok(Self {
206 id,
207 workspace_id,
208 title,
209 })
210 }
211
212 /// Returns the portable conversation identifier.
213 ///
214 /// # Examples
215 ///
216 /// ```
217 /// use agents_kit::conversation::ConversationInfo;
218 /// let id = "conversation_00000000000000000000000000000000".parse()?;
219 /// let workspace = "workspace_00000000000000000000000000000000".parse()?;
220 /// let info = ConversationInfo::new(id, workspace, Some("Review".into()))?;
221 /// assert!(info.id().as_str().starts_with("conversation_"));
222 /// # Ok::<(), agents_kit::KitError>(())
223 /// ```
224 pub fn id(&self) -> &ConversationId {
225 &self.id
226 }
227
228 /// Returns the portable identifier of the owning workspace.
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use agents_kit::conversation::ConversationInfo;
234 /// let id = "conversation_00000000000000000000000000000000".parse()?;
235 /// let workspace = "workspace_00000000000000000000000000000000".parse()?;
236 /// let info = ConversationInfo::new(id, workspace, Some("Review".into()))?;
237 /// assert!(info.workspace_id().as_str().starts_with("workspace_"));
238 /// # Ok::<(), agents_kit::KitError>(())
239 /// ```
240 pub fn workspace_id(&self) -> &WorkspaceId {
241 &self.workspace_id
242 }
243
244 /// Returns the configured title, when present.
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use agents_kit::conversation::ConversationInfo;
250 /// let id = "conversation_00000000000000000000000000000000".parse()?;
251 /// let workspace = "workspace_00000000000000000000000000000000".parse()?;
252 /// let info = ConversationInfo::new(id, workspace, Some("Review".into()))?;
253 /// assert_eq!(info.title(), Some("Review"));
254 /// # Ok::<(), agents_kit::KitError>(())
255 /// ```
256 pub fn title(&self) -> Option<&str> {
257 self.title.as_deref()
258 }
259
260 fn validate_title(title: Option<&str>) -> Result<(), KitError> {
261 if title.is_some_and(|value| value.is_empty() || value.trim() != value) {
262 return Err(KitError::invalid_input(
263 "conversation title must be nonempty and unpadded when present",
264 ));
265 }
266 Ok(())
267 }
268}
269
270/// Validated inputs for creating a conversation.
271///
272/// # Examples
273///
274/// ```
275/// use agents_kit::conversation::NewConversation;
276///
277/// let request = NewConversation::new(
278/// Some("incident review".to_owned()),
279/// None,
280/// Vec::new(),
281/// )?;
282/// assert_eq!(request.title(), Some("incident review"));
283/// # Ok::<(), agents_kit::KitError>(())
284/// ```
285#[derive(Clone, Debug, Eq, PartialEq)]
286pub struct NewConversation {
287 title: Option<String>,
288 defaults: Option<WorkspaceDefaults>,
289 knowledge_sources: Vec<KnowledgeSource>,
290}
291
292impl NewConversation {
293 /// Validates and constructs conversation-creation inputs.
294 ///
295 /// # Errors
296 ///
297 /// Returns `InvalidInput` for an empty or padded title, an invalid knowledge policy/source, or duplicate source IDs. Construction performs no native I/O.
298 ///
299 /// # Examples
300 ///
301 /// ```
302 /// use agents_kit::conversation::NewConversation;
303 /// let request = NewConversation::new(Some("Review".into()), None, Vec::new())?;
304 /// assert_eq!(request.title(), Some("Review"));
305 /// assert!(request.defaults().is_none());
306 /// assert!(request.knowledge_sources().is_empty());
307 /// # Ok::<(), agents_kit::KitError>(())
308 /// ```
309 pub fn new(
310 title: Option<String>,
311 defaults: Option<WorkspaceDefaults>,
312 knowledge_sources: Vec<KnowledgeSource>,
313 ) -> Result<Self, KitError> {
314 ConversationInfo::validate_title(title.as_deref())?;
315 if let Some(defaults) = &defaults
316 && let Some(policy) = defaults.knowledge_policy()
317 {
318 policy.validate()?;
319 }
320 let mut ids = std::collections::BTreeSet::new();
321 for source in &knowledge_sources {
322 source.validate()?;
323 if !ids.insert(source.source_id()) {
324 return Err(KitError::invalid_input(
325 "conversation knowledge source ids must be unique",
326 ));
327 }
328 }
329 Ok(Self {
330 title,
331 defaults,
332 knowledge_sources,
333 })
334 }
335
336 /// Returns the requested title, when present.
337 ///
338 /// # Examples
339 ///
340 /// ```
341 /// use agents_kit::conversation::NewConversation;
342 /// let request = NewConversation::new(Some("Review".into()), None, Vec::new())?;
343 /// assert_eq!(request.title(), Some("Review"));
344 /// assert!(request.defaults().is_none());
345 /// assert!(request.knowledge_sources().is_empty());
346 /// # Ok::<(), agents_kit::KitError>(())
347 /// ```
348 pub fn title(&self) -> Option<&str> {
349 self.title.as_deref()
350 }
351
352 /// Returns conversation-level default overrides, when present.
353 ///
354 /// # Examples
355 ///
356 /// ```
357 /// use agents_kit::conversation::NewConversation;
358 /// let request = NewConversation::new(Some("Review".into()), None, Vec::new())?;
359 /// assert_eq!(request.title(), Some("Review"));
360 /// assert!(request.defaults().is_none());
361 /// assert!(request.knowledge_sources().is_empty());
362 /// # Ok::<(), agents_kit::KitError>(())
363 /// ```
364 pub fn defaults(&self) -> Option<&WorkspaceDefaults> {
365 self.defaults.as_ref()
366 }
367
368 /// Returns the initial managed-knowledge sources.
369 ///
370 /// # Examples
371 ///
372 /// ```
373 /// use agents_kit::conversation::NewConversation;
374 /// let request = NewConversation::new(Some("Review".into()), None, Vec::new())?;
375 /// assert_eq!(request.title(), Some("Review"));
376 /// assert!(request.defaults().is_none());
377 /// assert!(request.knowledge_sources().is_empty());
378 /// # Ok::<(), agents_kit::KitError>(())
379 /// ```
380 pub fn knowledge_sources(&self) -> &[KnowledgeSource] {
381 &self.knowledge_sources
382 }
383}
384
385/// Public implementation contract for one live portable conversation.
386///
387/// A caller workspace returns its own concrete conversation implementation through
388/// [`WorkspaceBackend::Conversation`](crate::WorkspaceBackend::Conversation).
389/// Runtime composition can instead use a caller-owned exhaustive enum hierarchy.
390/// Prompting reaches a terminal portable result, abort joins active work, and close
391/// permanently closes the durable conversation.
392///
393/// # Examples
394///
395/// Local construction and metadata use no provider requests. The runtime stays
396/// alive through consuming conversation, workspace and Agent cleanup.
397///
398/// ```
399/// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
400/// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
401/// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
402/// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
403/// # use agents_kit::conversation::NewConversation;
404/// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
405/// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
406/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
407/// # runtime.block_on(async {
408/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
409/// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
410/// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
411/// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
412/// # let provider = Provider::from_config(ProviderConfig::new(
413/// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
414/// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
415/// # let cwd = std::env::current_dir()?;
416/// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
417/// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
418/// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
419/// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
420/// let info = conversation.info().await?;
421/// assert_eq!(info.id(), &conversation.id());
422/// # conversation.close().await?;
423/// # workspace.close().await?;
424/// # agent.shutdown().await?;
425/// # Ok::<(), Box<dyn std::error::Error>>(())
426/// # })?;
427/// # Ok::<(), Box<dyn std::error::Error>>(())
428/// ```
429pub trait ConversationBackend: Send + Sync + 'static {
430 /// Returns the durable portable conversation identity.
431 ///
432 /// # Examples
433 ///
434 /// This local Custom conversation uses memory Lance and performs no inference.
435 ///
436 /// ```
437 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
438 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
439 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
440 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
441 /// # use agents_kit::conversation::NewConversation;
442 /// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
443 /// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
444 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
445 /// # runtime.block_on(async {
446 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
447 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
448 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
449 /// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
450 /// # let provider = Provider::from_config(ProviderConfig::new(
451 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
452 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
453 /// # let cwd = std::env::current_dir()?;
454 /// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
455 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
456 /// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
457 /// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
458 /// let id = conversation.id();
459 /// assert!(id.as_str().starts_with("conversation_"));
460 /// # conversation.close().await?;
461 /// # workspace.close().await?;
462 /// # agent.shutdown().await?;
463 /// # Ok::<(), Box<dyn std::error::Error>>(())
464 /// # })?;
465 /// # Ok::<(), Box<dyn std::error::Error>>(())
466 /// ```
467 fn id(&self) -> ConversationId;
468
469 /// Reads the current descriptive conversation state.
470 ///
471 /// # Errors
472 ///
473 /// Returns `Closed` for a terminal handle, `Conflict` for stale ownership/revision, or a typed storage/backend failure.
474 ///
475 /// # Examples
476 ///
477 /// This local Custom conversation uses memory Lance and performs no inference.
478 ///
479 /// ```
480 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
481 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
482 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
483 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
484 /// # use agents_kit::conversation::NewConversation;
485 /// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
486 /// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
487 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
488 /// # runtime.block_on(async {
489 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
490 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
491 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
492 /// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
493 /// # let provider = Provider::from_config(ProviderConfig::new(
494 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
495 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
496 /// # let cwd = std::env::current_dir()?;
497 /// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
498 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
499 /// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
500 /// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
501 /// let info = conversation.info().await?;
502 /// assert_eq!(info.id(), &conversation.id());
503 /// assert_eq!(info.title(), Some("Review"));
504 /// # conversation.close().await?;
505 /// # workspace.close().await?;
506 /// # agent.shutdown().await?;
507 /// # Ok::<(), Box<dyn std::error::Error>>(())
508 /// # })?;
509 /// # Ok::<(), Box<dyn std::error::Error>>(())
510 /// ```
511 fn info(&self) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_;
512
513 /// Runs one foreground prompt through its terminal portable result.
514 ///
515 /// 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.
516 ///
517 /// # Errors
518 ///
519 /// 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.
520 ///
521 /// # Examples
522 ///
523 /// Requires a constructed live conversation and its configured native process or provider.
524 ///
525 /// ```no_run
526 /// use agents_kit::{ConversationBackend, KitError};
527 /// async fn example<C: ConversationBackend>(conversation: &C) -> Result<(), KitError> {
528 /// let request = agents_kit::prompt::PromptRequest::new("Describe the current task")?;
529 /// let response = conversation.prompt(request).await?;
530 /// println!("{}", response.output());
531 /// Ok(())
532 /// }
533 /// ```
534 fn prompt(
535 &self,
536 request: PromptRequest,
537 ) -> impl Future<Output = Result<PromptResponse, KitError>> + Send + '_;
538
539 /// Starts real incremental observation of one foreground prompt.
540 ///
541 /// The default rejects unsupported streaming before accepting work. Override
542 /// using public [`PromptStream`] and [`PromptEvent`](crate::prompt::PromptEvent)
543 /// values; do not fabricate deltas from a terminal response. Dropping an
544 /// observer does not cancel accepted work or release the conversation slot.
545 ///
546 /// # Errors
547 ///
548 /// 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.
549 ///
550 /// # Examples
551 ///
552 /// Requires a constructed live conversation and its configured native process or provider.
553 ///
554 /// ```no_run
555 /// use agents_kit::{ConversationBackend, KitError};
556 /// async fn example<C: ConversationBackend>(conversation: &C) -> Result<(), KitError> {
557 /// use agents_kit::prompt::{PromptRequest, PromptEvent};
558 /// let mut stream = conversation.prompt_stream(PromptRequest::new("Describe the current task")?).await?;
559 /// while let Some(event) = stream.recv().await {
560 /// match event? {
561 /// PromptEvent::OutputDelta { text } => print!("{text}"),
562 /// PromptEvent::Completed(response) => println!("\nFinal: {}", response.output()),
563 /// }
564 /// }
565 /// Ok(())
566 /// }
567 /// ```
568 fn prompt_stream(
569 &self,
570 _request: PromptRequest,
571 ) -> impl Future<Output = Result<PromptStream, KitError>> + Send + '_ {
572 async {
573 Err(KitError::unsupported(
574 "incremental prompt output is unavailable",
575 ))
576 }
577 }
578
579 /// Aborts the active prompt, or succeeds when no prompt is active.
580 ///
581 /// 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.
582 ///
583 /// # Errors
584 ///
585 /// Returns a typed cancellation, timeout, conflict, lifecycle, or backend failure. Ambiguous cancellation retains recovery ownership; an error does not prove remote work stopped.
586 ///
587 /// # Examples
588 ///
589 /// This local Custom conversation uses memory Lance and performs no inference.
590 ///
591 /// ```
592 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
593 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
594 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
595 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
596 /// # use agents_kit::conversation::NewConversation;
597 /// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
598 /// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
599 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
600 /// # runtime.block_on(async {
601 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
602 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
603 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
604 /// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
605 /// # let provider = Provider::from_config(ProviderConfig::new(
606 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
607 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
608 /// # let cwd = std::env::current_dir()?;
609 /// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
610 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
611 /// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
612 /// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
613 /// // Idle abort is local and does not make a model request.
614 /// conversation.abort().await?;
615 /// # conversation.close().await?;
616 /// # workspace.close().await?;
617 /// # agent.shutdown().await?;
618 /// # Ok::<(), Box<dyn std::error::Error>>(())
619 /// # })?;
620 /// # Ok::<(), Box<dyn std::error::Error>>(())
621 /// ```
622 fn abort(&self) -> impl Future<Output = Result<(), KitError>> + Send + '_;
623
624 /// Reads a validated workspace-relative file through the implementation.
625 ///
626 /// 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.
627 ///
628 /// # Errors
629 ///
630 /// 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`.
631 ///
632 /// # Examples
633 ///
634 /// Requires a constructed live conversation and its configured native process or provider.
635 ///
636 /// ```no_run
637 /// use agents_kit::{ConversationBackend, KitError};
638 /// async fn example<C: ConversationBackend>(conversation: &C) -> Result<(), KitError> {
639 /// let file = conversation.read_file("README.md".into()).await?;
640 /// assert_eq!(file.path(), std::path::Path::new("README.md"));
641 /// let size = file.text().map(str::len).or_else(|| file.bytes().map(<[u8]>::len));
642 /// assert!(size.is_some());
643 /// Ok(())
644 /// }
645 /// ```
646 fn read_file(
647 &self,
648 path: PathBuf,
649 ) -> impl Future<Output = Result<FileRead, KitError>> + Send + '_;
650
651 /// Claims the conversation's exclusive durable notification receiver.
652 ///
653 /// # Errors
654 ///
655 /// 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`.
656 ///
657 /// # Examples
658 ///
659 /// This local Custom conversation uses memory Lance and performs no inference.
660 ///
661 /// ```
662 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
663 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
664 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
665 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
666 /// # use agents_kit::conversation::NewConversation;
667 /// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
668 /// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
669 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
670 /// # runtime.block_on(async {
671 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
672 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
673 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
674 /// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
675 /// # let provider = Provider::from_config(ProviderConfig::new(
676 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
677 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
678 /// # let cwd = std::env::current_dir()?;
679 /// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
680 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
681 /// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
682 /// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
683 /// let receiver = conversation.notification_stream()?;
684 /// drop(receiver); // release without acknowledging anything
685 /// let replacement = conversation.notification_stream()?;
686 /// drop(replacement);
687 /// # conversation.close().await?;
688 /// # workspace.close().await?;
689 /// # agent.shutdown().await?;
690 /// # Ok::<(), Box<dyn std::error::Error>>(())
691 /// # })?;
692 /// # Ok::<(), Box<dyn std::error::Error>>(())
693 /// ```
694 fn notification_stream(&self) -> Result<ConversationNotificationReceiver, KitError>;
695
696 /// Submits a resolution for an unresolved portable notification.
697 ///
698 /// # Errors
699 ///
700 /// 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.
701 ///
702 /// # Examples
703 ///
704 /// Requires a constructed live conversation and its configured native process or provider.
705 ///
706 /// ```no_run
707 /// use agents_kit::{ConversationBackend, KitError};
708 /// async fn example<C: ConversationBackend>(conversation: &C, id: agents_kit::identity::NotificationId) -> Result<(), KitError> {
709 /// use agents_kit::notification::NotificationResolution;
710 /// // The caller has inspected this pending approval and chosen denial.
711 /// conversation.resolve_notification(id, NotificationResolution::Deny {
712 /// reason: "Outside the approved task".into(),
713 /// }).await?;
714 /// Ok(())
715 /// }
716 /// ```
717 fn resolve_notification(
718 &self,
719 id: NotificationId,
720 resolution: NotificationResolution,
721 ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
722
723 /// Permanently closes the conversation and consumes the implementation.
724 ///
725 /// 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.
726 ///
727 /// # Errors
728 ///
729 /// Returns a typed cancellation, reconciliation, storage, or cleanup failure. The handle remains consumed on failure; durable recovery obligations remain authoritative.
730 ///
731 /// # Examples
732 ///
733 /// This local Custom conversation uses memory Lance and performs no inference.
734 ///
735 /// ```
736 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
737 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
738 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
739 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
740 /// # use agents_kit::conversation::NewConversation;
741 /// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
742 /// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
743 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
744 /// # runtime.block_on(async {
745 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
746 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
747 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
748 /// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
749 /// # let provider = Provider::from_config(ProviderConfig::new(
750 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
751 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
752 /// # let cwd = std::env::current_dir()?;
753 /// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
754 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
755 /// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
756 /// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
757 /// conversation.close().await?; // consuming terminal cleanup
758 /// # workspace.close().await?;
759 /// # agent.shutdown().await?;
760 /// # Ok::<(), Box<dyn std::error::Error>>(())
761 /// # })?;
762 /// # Ok::<(), Box<dyn std::error::Error>>(())
763 /// ```
764 fn close(self) -> impl Future<Output = Result<(), KitError>> + Send
765 where
766 Self: Sized;
767}
768
769/// Opaque live handle for one portable conversation.
770///
771/// # Examples
772///
773/// Local Custom construction needs no running provider until a prompt is submitted.
774///
775/// ```
776/// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend, ConversationBackend};
777/// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
778/// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
779/// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults, WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
780/// # use agents_kit::conversation::NewConversation;
781/// # use providers_kit::{Provider, ProviderConfig, config::{ProviderKind, OllamaConfig, TransportConfig}};
782/// # use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
783/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
784/// # runtime.block_on(async {
785/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
786/// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
787/// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
788/// # let config = AgentConfig::new(custom.into(), std::time::Duration::from_secs(10), std::num::NonZeroUsize::MIN)?;
789/// # let provider = Provider::from_config(ProviderConfig::new(
790/// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
791/// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
792/// # let cwd = std::env::current_dir()?;
793/// # let context = WorkspaceContext::new(cwd.clone(), vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
794/// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
795/// # let workspace = agent.new_workspace(NewWorkspace::new("docs", context, WorkspaceDefaults::empty())?).await?;
796/// # let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, Vec::new())?).await?;
797/// assert_eq!(conversation.info().await?.title(), Some("Review"));
798/// # conversation.close().await?;
799/// # workspace.close().await?;
800/// # agent.shutdown().await?;
801/// # Ok::<(), Box<dyn std::error::Error>>(())
802/// # })?;
803/// # Ok::<(), Box<dyn std::error::Error>>(())
804/// ```
805pub struct Conversation<
806 S: StateStoreBackend = storage_kit::StateStore,
807 D: DatabaseBackend = storage_kit::Database,
808 P: ProviderBackend = providers_kit::Provider,
809 T: ToolsetBackend = tools_kit::Toolset,
810> {
811 inner: ConversationInner<S, D, P, T>,
812}
813
814enum ConversationInner<
815 S: StateStoreBackend,
816 D: DatabaseBackend,
817 P: ProviderBackend,
818 T: ToolsetBackend,
819> {
820 Codex(codex::conversations::CodexConversation<S, D, T>),
821 OpenCode(opencode::conversations::OpenCodeConversation<S, D>),
822 Custom(custom::CustomConversation<S, D, P, T>),
823}
824
825impl<S, D, P, T> Conversation<S, D, P, T>
826where
827 S: StateStoreBackend,
828 D: DatabaseBackend,
829 P: ProviderBackend,
830 T: ToolsetBackend,
831{
832 pub(crate) fn codex(backend: codex::conversations::CodexConversation<S, D, T>) -> Self {
833 Self {
834 inner: ConversationInner::Codex(backend),
835 }
836 }
837
838 pub(crate) fn opencode(backend: opencode::conversations::OpenCodeConversation<S, D>) -> Self {
839 Self {
840 inner: ConversationInner::OpenCode(backend),
841 }
842 }
843
844 pub(crate) fn custom(backend: custom::CustomConversation<S, D, P, T>) -> Self {
845 Self {
846 inner: ConversationInner::Custom(backend),
847 }
848 }
849}
850
851impl<S, D, P, T> ConversationBackend for ConversationInner<S, D, P, T>
852where
853 S: StateStoreBackend,
854 D: DatabaseBackend,
855 P: ProviderBackend,
856 T: ToolsetBackend,
857{
858 fn id(&self) -> ConversationId {
859 match self {
860 ConversationInner::Codex(backend) => backend.id(),
861 ConversationInner::OpenCode(backend) => backend.id(),
862 ConversationInner::Custom(backend) => backend.id(),
863 }
864 }
865
866 async fn info(&self) -> Result<ConversationInfo, KitError> {
867 match self {
868 ConversationInner::Codex(backend) => backend.info().await,
869 ConversationInner::OpenCode(backend) => backend.info().await,
870 ConversationInner::Custom(backend) => backend.info().await,
871 }
872 }
873
874 async fn prompt(&self, request: PromptRequest) -> Result<PromptResponse, KitError> {
875 match self {
876 ConversationInner::Codex(backend) => backend.prompt(request).await,
877 ConversationInner::OpenCode(backend) => backend.prompt(request).await,
878 ConversationInner::Custom(backend) => backend.prompt(request).await,
879 }
880 }
881
882 async fn prompt_stream(&self, request: PromptRequest) -> Result<PromptStream, KitError> {
883 match self {
884 ConversationInner::Codex(backend) => backend.prompt_stream(request).await,
885 ConversationInner::OpenCode(backend) => backend.prompt_stream(request).await,
886 ConversationInner::Custom(backend) => backend.prompt_stream(request).await,
887 }
888 }
889
890 async fn abort(&self) -> Result<(), KitError> {
891 match self {
892 ConversationInner::Codex(backend) => backend.abort().await,
893 ConversationInner::OpenCode(backend) => backend.abort().await,
894 ConversationInner::Custom(backend) => backend.abort().await,
895 }
896 }
897
898 async fn read_file(&self, path: PathBuf) -> Result<FileRead, KitError> {
899 match self {
900 ConversationInner::Codex(backend) => backend.read_file(path).await,
901 ConversationInner::OpenCode(backend) => backend.read_file(path).await,
902 ConversationInner::Custom(backend) => backend.read_file(path).await,
903 }
904 }
905
906 fn notification_stream(&self) -> Result<ConversationNotificationReceiver, KitError> {
907 match self {
908 ConversationInner::Codex(backend) => backend.notification_stream(),
909 ConversationInner::OpenCode(backend) => backend.notification_stream(),
910 ConversationInner::Custom(backend) => backend.notification_stream(),
911 }
912 }
913
914 async fn resolve_notification(
915 &self,
916 id: NotificationId,
917 resolution: NotificationResolution,
918 ) -> Result<(), KitError> {
919 match self {
920 ConversationInner::Codex(backend) => backend.resolve_notification(id, resolution).await,
921 ConversationInner::OpenCode(backend) => {
922 backend.resolve_notification(id, resolution).await
923 }
924 ConversationInner::Custom(backend) => {
925 backend.resolve_notification(id, resolution).await
926 }
927 }
928 }
929
930 async fn close(self) -> Result<(), KitError> {
931 match self {
932 ConversationInner::Codex(backend) => backend.close().await,
933 ConversationInner::OpenCode(backend) => backend.close().await,
934 ConversationInner::Custom(backend) => backend.close().await,
935 }
936 }
937}
938
939impl<S, D, P, T> ConversationBackend for Conversation<S, D, P, T>
940where
941 S: StateStoreBackend,
942 D: DatabaseBackend,
943 P: ProviderBackend,
944 T: ToolsetBackend,
945{
946 fn id(&self) -> ConversationId {
947 self.inner.id()
948 }
949
950 async fn info(&self) -> Result<ConversationInfo, KitError> {
951 self.inner.info().await.map_err(|error| {
952 error
953 .at_operation(crate::error::Operation::ConversationInfo)
954 .in_phase(crate::error::ErrorPhase::StateRead)
955 })
956 }
957
958 async fn prompt(&self, request: PromptRequest) -> Result<PromptResponse, KitError> {
959 self.inner.prompt(request).await.map_err(|error| {
960 error
961 .at_operation(crate::error::Operation::Prompt)
962 .in_phase(crate::error::ErrorPhase::ModelGeneration)
963 })
964 }
965
966 async fn prompt_stream(&self, request: PromptRequest) -> Result<PromptStream, KitError> {
967 self.inner.prompt_stream(request).await.map_err(|error| {
968 error
969 .at_operation(crate::error::Operation::Prompt)
970 .in_phase(crate::error::ErrorPhase::ModelGeneration)
971 })
972 }
973
974 async fn abort(&self) -> Result<(), KitError> {
975 self.inner.abort().await.map_err(|error| {
976 error
977 .at_operation(crate::error::Operation::Abort)
978 .in_phase(crate::error::ErrorPhase::Cancellation)
979 })
980 }
981
982 async fn read_file(&self, path: PathBuf) -> Result<FileRead, KitError> {
983 self.inner.read_file(path).await.map_err(|error| {
984 error
985 .at_operation(crate::error::Operation::ReadFile)
986 .in_phase(crate::error::ErrorPhase::Admission)
987 })
988 }
989
990 fn notification_stream(&self) -> Result<ConversationNotificationReceiver, KitError> {
991 self.inner.notification_stream().map_err(|error| {
992 error
993 .at_operation(crate::error::Operation::NotificationStream)
994 .in_phase(crate::error::ErrorPhase::Admission)
995 })
996 }
997
998 async fn resolve_notification(
999 &self,
1000 id: NotificationId,
1001 resolution: NotificationResolution,
1002 ) -> Result<(), KitError> {
1003 self.inner
1004 .resolve_notification(id, resolution)
1005 .await
1006 .map_err(|error| {
1007 error
1008 .at_operation(crate::error::Operation::ResolveNotification)
1009 .in_phase(crate::error::ErrorPhase::InteractionResolution)
1010 })
1011 }
1012
1013 async fn close(self) -> Result<(), KitError> {
1014 self.inner.close().await.map_err(|error| {
1015 error
1016 .at_operation(crate::error::Operation::CloseConversation)
1017 .in_phase(crate::error::ErrorPhase::Lifecycle)
1018 })
1019 }
1020}