Skip to main content

agents_kit/
agent.rs

1use std::future::Future;
2use tools_kit::ToolsetBackend;
3
4use providers_kit::ProviderBackend;
5use storage_kit::{DatabaseBackend, StateStoreBackend};
6
7use crate::backends::{codex, custom, opencode};
8use crate::{
9    AgentId, DeleteWorkspaceRequest, IngestKnowledgeRequest, KitError, ModelInfo, NewWorkspace,
10    UpdateWorkspaceRequest, Workspace, WorkspaceId, WorkspaceInfo, WorkspaceUpdateReadiness,
11};
12
13/// A descriptive snapshot of one durable agent identity.
14///
15/// Constructing this DTO validates its descriptive fields only; it does not
16/// create an Agent or establish that the identity exists in storage.
17///
18/// # Examples
19///
20/// ```
21/// use agents_kit::identity::AgentInfo;
22/// let id = "agent_0123456789abcdef0123456789abcdef".parse()?;
23/// let info = AgentInfo::new(id, Some("Reviewer".to_owned()))?;
24/// assert_eq!(info.display_name(), Some("Reviewer"));
25/// # Ok::<(), agents_kit::KitError>(())
26/// ```
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct AgentInfo {
29    id: AgentId,
30    display_name: Option<String>,
31}
32
33impl AgentInfo {
34    /// Validates and constructs descriptive agent information.
35    ///
36    /// Display names, when present, must be nonempty and must not contain
37    /// leading or trailing whitespace.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`crate::error::ErrorKind::InvalidInput`] when `display_name` violates the
42    /// portable naming contract.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use agents_kit::identity::AgentInfo;
48    /// let id = "agent_0123456789abcdef0123456789abcdef".parse()?;
49    /// assert!(AgentInfo::new(id, Some(" padded".to_owned())).is_err());
50    /// # Ok::<(), agents_kit::KitError>(())
51    /// ```
52    pub fn new(id: AgentId, display_name: Option<String>) -> Result<Self, KitError> {
53        if display_name
54            .as_deref()
55            .is_some_and(|value| value.is_empty() || value.trim() != value)
56        {
57            return Err(KitError::invalid_input(
58                "agent display name must be nonempty and unpadded when present",
59            ));
60        }
61        Ok(Self { id, display_name })
62    }
63
64    /// Returns the portable durable identity.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use agents_kit::identity::AgentInfo;
70    /// let id = "agent_0123456789abcdef0123456789abcdef".parse()?;
71    /// let info = AgentInfo::new(id, None)?;
72    /// assert_eq!(info.id().as_str(), "agent_0123456789abcdef0123456789abcdef");
73    /// # Ok::<(), agents_kit::KitError>(())
74    /// ```
75    pub fn id(&self) -> &AgentId {
76        &self.id
77    }
78
79    /// Returns the configured display name, when present.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use agents_kit::identity::AgentInfo;
85    /// let id = "agent_0123456789abcdef0123456789abcdef".parse()?;
86    /// let info = AgentInfo::new(id, None)?;
87    /// assert_eq!(info.display_name(), None);
88    /// # Ok::<(), agents_kit::KitError>(())
89    /// ```
90    pub fn display_name(&self) -> Option<&str> {
91        self.display_name.as_deref()
92    }
93}
94
95/// Public implementation contract for a complete portable agent runtime.
96///
97/// Built-ins and caller implementations use this behavioral boundary directly.
98/// A caller implementation returns its own concrete [`Workspace`](Self::Workspace)
99/// implementation; runtime composition can instead use a caller-owned exhaustive
100/// enum hierarchy. Accepted work must reach its documented durable terminal state
101/// even if a caller drops the awaiting future. Shutdown seals new work, invalidates
102/// live descendants, and settles accepted cleanup.
103///
104/// # Examples
105///
106/// This local Custom agent uses in-memory Lance and sends no network requests.
107///
108/// ```
109/// # use std::{num::NonZeroUsize, time::Duration};
110/// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
111/// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
112/// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
113/// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
114/// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
115/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
116/// # runtime.block_on(async {
117/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
118/// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
119/// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
120/// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
121/// # let provider = Provider::from_config(ProviderConfig::new(
122/// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
123/// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
124/// # let outcome = async {
125/// use agents_kit::identity::AgentInfo;
126/// use agents_kit::KitError;
127/// async fn inspect<A: AgentBackend>(agent: &A) -> Result<AgentInfo, KitError> {
128///     let info = agent.info().await?;
129///     assert_eq!(info.id(), &agent.id());
130///     Ok(info)
131/// }
132/// assert_eq!(inspect(&agent).await?.id(), &agent.id());
133/// # Ok::<(), Box<dyn std::error::Error>>(())
134/// # }.await;
135/// # let cleanup = agent.shutdown().await;
136/// # outcome?;
137/// # cleanup?;
138/// # Ok::<(), Box<dyn std::error::Error>>(())
139/// # })?;
140/// # Ok::<(), Box<dyn std::error::Error>>(())
141/// ```
142/// # Caller-owned concrete composition
143///
144/// This is an illustrative application enum hierarchy, not a new durable engine
145/// or evidence of native capability. It supports one supplied family here and
146/// delegates every operation, including errors and consuming cleanup, to its real
147/// owner. Add another *concrete* family with matching child enum arms when your
148/// application needs it. There is no `Agent::from_backend` ingress, no erased
149/// backend, and no independent Workspace/Conversation type parameter.
150///
151/// The example executes local creation, metadata access and cleanup. The supplied
152/// Custom implementation retains responsibility for persistence, revision fencing,
153/// notifications, prompt admission and shutdown. It performs no model request.
154///
155/// ```
156/// use std::{future::Future, num::NonZeroUsize, path::PathBuf, time::Duration};
157/// use agents_kit::{Agent, AgentBackend, AgentBuilder, AgentStorage, Conversation,
158///     ConversationBackend, KitError, Workspace, WorkspaceBackend};
159/// use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
160/// use agents_kit::conversation::{ConversationInfo, FileRead, NewConversation};
161/// use agents_kit::identity::{AgentId, AgentInfo, ConversationId, NotificationId, WorkspaceId};
162/// use agents_kit::knowledge::IngestKnowledgeRequest;
163/// use agents_kit::model::{ModelInfo, ModelSelector};
164/// use agents_kit::notification::{ConversationNotificationReceiver, NotificationResolution};
165/// use agents_kit::prompt::{PromptRequest, PromptResponse, PromptStream};
166/// use agents_kit::tool::{ToolInfo, ToolSelection};
167/// use agents_kit::workspace::{DeleteWorkspaceRequest, NewWorkspace, UpdateWorkspaceRequest,
168///     WorkspaceContext, WorkspaceDefaults, WorkspaceExecutionPolicy, WorkspaceInfo,
169///     WorkspaceNetworkAccess, WorkspaceRoot, WorkspaceRootAccess, WorkspaceSandboxMode,
170///     WorkspaceUpdateReadiness};
171/// use providers_kit::{Provider, ProviderConfig,
172///     config::{OllamaConfig, ProviderKind, TransportConfig}};
173/// use storage_kit::{StateStore,
174///     state::config::{LanceStateStoreConfig, StateStoreConfig}};
175///
176/// enum ApplicationAgent { Supplied(Agent) }
177/// enum ApplicationWorkspace { Supplied(Workspace) }
178/// enum ApplicationConversation { Supplied(Conversation) }
179///
180/// impl AgentBackend for ApplicationAgent {
181///     type Workspace = ApplicationWorkspace;
182///     fn id(&self) -> AgentId {
183///         match self { Self::Supplied(value) => value.id() }
184///     }
185///     async fn info(&self) -> Result<AgentInfo, KitError> {
186///         match self { Self::Supplied(value) => value.info().await }
187///     }
188///     async fn new_workspace(&self, request: NewWorkspace) -> Result<Self::Workspace, KitError> {
189///         match self { Self::Supplied(value) => value.new_workspace(request).await.map(ApplicationWorkspace::Supplied) }
190///     }
191///     async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, KitError> {
192///         match self { Self::Supplied(value) => value.list_workspaces().await }
193///     }
194///     async fn workspace_info(&self, id: WorkspaceId) -> Result<WorkspaceInfo, KitError> {
195///         match self { Self::Supplied(value) => value.workspace_info(id).await }
196///     }
197///     async fn get_workspace(&self, id: WorkspaceId) -> Result<Self::Workspace, KitError> {
198///         match self { Self::Supplied(value) => value.get_workspace(id).await.map(ApplicationWorkspace::Supplied) }
199///     }
200///     fn check_workspace_update(&self, request: &UpdateWorkspaceRequest)
201///         -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_ {
202///         // Construct the delegated future now; it does not borrow the request.
203///         match self { Self::Supplied(value) => value.check_workspace_update(request) }
204///     }
205///     async fn update_workspace(&self, request: UpdateWorkspaceRequest) -> Result<WorkspaceInfo, KitError> {
206///         match self { Self::Supplied(value) => value.update_workspace(request).await }
207///     }
208///     async fn delete_workspace(&self, request: DeleteWorkspaceRequest) -> Result<(), KitError> {
209///         match self { Self::Supplied(value) => value.delete_workspace(request).await }
210///     }
211///     async fn list_models(&self) -> Result<Vec<ModelInfo>, KitError> {
212///         match self { Self::Supplied(value) => value.list_models().await }
213///     }
214///     async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
215///         match self { Self::Supplied(value) => value.ingest_knowledge(request).await }
216///     }
217///     async fn shutdown(self) -> Result<(), KitError> {
218///         match self { Self::Supplied(value) => value.shutdown().await }
219///     }
220/// }
221/// impl WorkspaceBackend for ApplicationWorkspace {
222///     type Conversation = ApplicationConversation;
223///     fn id(&self) -> WorkspaceId {
224///         match self { Self::Supplied(value) => value.id() }
225///     }
226///     async fn info(&self) -> Result<WorkspaceInfo, KitError> {
227///         match self { Self::Supplied(value) => value.info().await }
228///     }
229///     async fn new_conversation(&self, request: NewConversation) -> Result<Self::Conversation, KitError> {
230///         match self { Self::Supplied(value) => value.new_conversation(request).await.map(ApplicationConversation::Supplied) }
231///     }
232///     async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
233///         match self { Self::Supplied(value) => value.list_conversations().await }
234///     }
235///     async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
236///         match self { Self::Supplied(value) => value.conversation_info(id).await }
237///     }
238///     async fn get_conversation(&self, id: ConversationId) -> Result<Self::Conversation, KitError> {
239///         match self { Self::Supplied(value) => value.get_conversation(id).await.map(ApplicationConversation::Supplied) }
240///     }
241///     async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
242///         match self { Self::Supplied(value) => value.list_tools().await }
243///     }
244///     async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
245///         match self { Self::Supplied(value) => value.ingest_knowledge(request).await }
246///     }
247///     async fn close(self) -> Result<(), KitError> {
248///         match self { Self::Supplied(value) => value.close().await }
249///     }
250/// }
251/// impl ConversationBackend for ApplicationConversation {
252///     fn id(&self) -> ConversationId {
253///         match self { Self::Supplied(value) => value.id() }
254///     }
255///     async fn info(&self) -> Result<ConversationInfo, KitError> {
256///         match self { Self::Supplied(value) => value.info().await }
257///     }
258///     async fn prompt(&self, request: PromptRequest) -> Result<PromptResponse, KitError> {
259///         match self { Self::Supplied(value) => value.prompt(request).await }
260///     }
261///     async fn prompt_stream(&self, request: PromptRequest) -> Result<PromptStream, KitError> {
262///         match self { Self::Supplied(value) => value.prompt_stream(request).await }
263///     }
264///     async fn abort(&self) -> Result<(), KitError> {
265///         match self { Self::Supplied(value) => value.abort().await }
266///     }
267///     async fn read_file(&self, path: PathBuf) -> Result<FileRead, KitError> {
268///         match self { Self::Supplied(value) => value.read_file(path).await }
269///     }
270///     fn notification_stream(&self) -> Result<ConversationNotificationReceiver, KitError> {
271///         match self { Self::Supplied(value) => value.notification_stream() }
272///     }
273///     async fn resolve_notification(&self, id: NotificationId, resolution: NotificationResolution) -> Result<(), KitError> {
274///         match self { Self::Supplied(value) => value.resolve_notification(id, resolution).await }
275///     }
276///     async fn close(self) -> Result<(), KitError> {
277///         match self { Self::Supplied(value) => value.close().await }
278///     }
279/// }
280///
281/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
282/// # runtime.block_on(async {
283/// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
284/// let config = AgentConfig::new(CustomAgentConfig::new(ModelSelector::new("exact-model")?,
285///     ToolSelection::None, CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?.into(),
286///     Duration::from_secs(10), NonZeroUsize::MIN)?;
287/// let provider = Provider::from_config(ProviderConfig::new(
288///     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
289/// let agent = ApplicationAgent::Supplied(AgentBuilder::create(config, AgentStorage::new(store))
290///     .with_provider(provider).build().await?);
291/// let outcome = async {
292///     let cwd = std::env::current_dir()?;
293///     let context = WorkspaceContext::new(cwd.clone(),
294///         vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
295///         WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
296///     let workspace = agent.new_workspace(NewWorkspace::new("caller", context, WorkspaceDefaults::empty())?).await?;
297///     let conversation = workspace.new_conversation(NewConversation::new(Some("Review".into()), None, vec![])?).await?;
298///     assert_eq!(conversation.info().await?.workspace_id(), &workspace.id());
299///     assert_eq!(workspace.list_conversations().await?.len(), 1);
300///     conversation.close().await?;
301///     workspace.close().await?;
302///     Ok::<(), Box<dyn std::error::Error>>(())
303/// }.await;
304/// let shutdown = agent.shutdown().await;
305/// outcome?;
306/// shutdown?;
307/// # Ok::<(), Box<dyn std::error::Error>>(())
308/// # })?;
309/// # Ok::<(), Box<dyn std::error::Error>>(())
310/// ```
311pub trait AgentBackend: Send + Sync + 'static {
312    /// Concrete workspace implementation produced by this agent.
313    type Workspace: crate::WorkspaceBackend;
314
315    /// Returns the durable portable agent identity.
316    ///
317    /// The identity remains stable across runtime reopen. This value identifies
318    /// the durable Agent; it does not expose backend routing or lower resources.
319    ///
320    /// # Examples
321    ///
322    /// This local Custom agent uses in-memory Lance and sends no network requests.
323    ///
324    /// ```
325    /// # use std::{num::NonZeroUsize, time::Duration};
326    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
327    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
328    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
329    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
330    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
331    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
332    /// # runtime.block_on(async {
333    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
334    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
335    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
336    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
337    /// # let provider = Provider::from_config(ProviderConfig::new(
338    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
339    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
340    /// # let outcome = async {
341    /// let identity = agent.id();
342    /// assert_eq!(agent.info().await?.id(), &identity);
343    /// # Ok::<(), Box<dyn std::error::Error>>(())
344    /// # }.await;
345    /// # let cleanup = agent.shutdown().await;
346    /// # outcome?;
347    /// # cleanup?;
348    /// # Ok::<(), Box<dyn std::error::Error>>(())
349    /// # })?;
350    /// # Ok::<(), Box<dyn std::error::Error>>(())
351    /// ```
352    fn id(&self) -> AgentId;
353
354    /// Loads the current descriptive agent information.
355    ///
356    /// # Errors
357    ///
358    /// Returns [`KitError`] if the runtime is closed, committed state cannot be
359    /// read, or stored facts contradict the portable record contract.
360    ///
361    /// # Examples
362    ///
363    /// This local Custom agent uses in-memory Lance and sends no network requests.
364    ///
365    /// ```
366    /// # use std::{num::NonZeroUsize, time::Duration};
367    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
368    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
369    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
370    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
371    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
372    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
373    /// # runtime.block_on(async {
374    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
375    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
376    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
377    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
378    /// # let provider = Provider::from_config(ProviderConfig::new(
379    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
380    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
381    /// # let outcome = async {
382    /// let info = agent.info().await?;
383    /// assert_eq!(info.id(), &agent.id());
384    /// # Ok::<(), Box<dyn std::error::Error>>(())
385    /// # }.await;
386    /// # let cleanup = agent.shutdown().await;
387    /// # outcome?;
388    /// # cleanup?;
389    /// # Ok::<(), Box<dyn std::error::Error>>(())
390    /// # })?;
391    /// # Ok::<(), Box<dyn std::error::Error>>(())
392    /// ```
393    fn info(&self) -> impl Future<Output = Result<AgentInfo, KitError>> + Send + '_;
394
395    /// Creates and publishes a workspace from validated portable configuration.
396    ///
397    /// Publishes a handle only after complete context/defaults, backend attachment,
398    /// and durable ownership agree. A new workspace begins at revision one.
399    ///
400    /// # Errors
401    ///
402    /// Returns [`KitError`] for closed or busy admission, unsupported execution
403    /// policy/integration composition, invalid input, dependency failure, or
404    /// deadline expiry. Accepted native setup must remain durably attached or
405    /// recovery-owned when an operation fails.
406    ///
407    /// # Examples
408    ///
409    /// This local Custom agent uses in-memory Lance and sends no network requests.
410    ///
411    /// ```
412    /// # use std::{num::NonZeroUsize, time::Duration};
413    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
414    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
415    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
416    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
417    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
418    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
419    /// # runtime.block_on(async {
420    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
421    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
422    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
423    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
424    /// # let provider = Provider::from_config(ProviderConfig::new(
425    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
426    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
427    /// # let outcome = async {
428    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
429    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
430    /// # let cwd = std::env::current_dir()?;
431    /// # let context = WorkspaceContext::new(cwd.clone(),
432    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
433    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
434    /// let workspace = agent.new_workspace(
435    ///     NewWorkspace::new("review", context, WorkspaceDefaults::empty())?,
436    /// ).await?;
437    /// assert_eq!(workspace.info().await?.revision(), 1);
438    /// workspace.close().await?;
439    /// # Ok::<(), Box<dyn std::error::Error>>(())
440    /// # }.await;
441    /// # let cleanup = agent.shutdown().await;
442    /// # outcome?;
443    /// # cleanup?;
444    /// # Ok::<(), Box<dyn std::error::Error>>(())
445    /// # })?;
446    /// # Ok::<(), Box<dyn std::error::Error>>(())
447    /// ```
448    fn new_workspace(
449        &self,
450        request: NewWorkspace,
451    ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_;
452
453    /// Lists the agent's durable workspaces.
454    ///
455    /// Inventory comes from committed records, not only currently opened handles.
456    /// The result does not claim live ownership of those workspaces.
457    ///
458    /// # Errors
459    ///
460    /// Returns [`KitError`] for a closed runtime, state-read failure, deadline
461    /// expiry, or inconsistent durable inventory.
462    ///
463    /// # Examples
464    ///
465    /// This local Custom agent uses in-memory Lance and sends no network requests.
466    ///
467    /// ```
468    /// # use std::{num::NonZeroUsize, time::Duration};
469    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
470    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
471    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
472    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
473    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
474    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
475    /// # runtime.block_on(async {
476    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
477    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
478    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
479    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
480    /// # let provider = Provider::from_config(ProviderConfig::new(
481    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
482    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
483    /// # let outcome = async {
484    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
485    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
486    /// # let cwd = std::env::current_dir()?;
487    /// # let context = WorkspaceContext::new(cwd.clone(),
488    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
489    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
490    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
491    /// let inventory = agent.list_workspaces().await?;
492    /// assert_eq!(inventory.len(), 1);
493    /// assert_eq!(inventory[0].id(), &workspace.id());
494    /// # workspace.close().await?;
495    /// # Ok::<(), Box<dyn std::error::Error>>(())
496    /// # }.await;
497    /// # let cleanup = agent.shutdown().await;
498    /// # outcome?;
499    /// # cleanup?;
500    /// # Ok::<(), Box<dyn std::error::Error>>(())
501    /// # })?;
502    /// # Ok::<(), Box<dyn std::error::Error>>(())
503    /// ```
504    fn list_workspaces(
505        &self,
506    ) -> impl Future<Output = Result<Vec<WorkspaceInfo>, KitError>> + Send + '_;
507
508    /// Loads descriptive information for a workspace identity.
509    ///
510    /// This reads committed metadata without claiming a live workspace handle.
511    /// Missing and foreign identities are both reported without revealing owners.
512    ///
513    /// # Errors
514    ///
515    /// Returns [`crate::error::ErrorKind::NotFound`] for a missing or foreign id,
516    /// or the owning lifecycle, state-read, integrity, or deadline error.
517    ///
518    /// # Examples
519    ///
520    /// This local Custom agent uses in-memory Lance and sends no network requests.
521    ///
522    /// ```
523    /// # use std::{num::NonZeroUsize, time::Duration};
524    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
525    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
526    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
527    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
528    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
529    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
530    /// # runtime.block_on(async {
531    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
532    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
533    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
534    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
535    /// # let provider = Provider::from_config(ProviderConfig::new(
536    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
537    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
538    /// # let outcome = async {
539    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
540    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
541    /// # let cwd = std::env::current_dir()?;
542    /// # let context = WorkspaceContext::new(cwd.clone(),
543    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
544    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
545    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
546    /// let info = agent.workspace_info(workspace.id()).await?;
547    /// assert_eq!(info.name(), "review");
548    /// assert_eq!(info.revision(), 1);
549    /// # workspace.close().await?;
550    /// # Ok::<(), Box<dyn std::error::Error>>(())
551    /// # }.await;
552    /// # let cleanup = agent.shutdown().await;
553    /// # outcome?;
554    /// # cleanup?;
555    /// # Ok::<(), Box<dyn std::error::Error>>(())
556    /// # })?;
557    /// # Ok::<(), Box<dyn std::error::Error>>(())
558    /// ```
559    fn workspace_info(
560        &self,
561        id: WorkspaceId,
562    ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
563
564    /// Reopens a durable workspace as a live handle.
565    ///
566    /// At most one live handle exists for a current workspace revision. Share it
567    /// by reference; a second current-revision lookup is busy. Reopen verifies
568    /// durable attachment and integration compatibility rather than inventing a
569    /// replacement workspace.
570    ///
571    /// # Errors
572    ///
573    /// Returns [`KitError`] for a missing/foreign id, an existing live claim,
574    /// closed runtime, incompatible reopen state, dependency failure, or timeout.
575    ///
576    /// # Examples
577    ///
578    /// This local Custom agent uses in-memory Lance and sends no network requests.
579    ///
580    /// ```
581    /// # use std::{num::NonZeroUsize, time::Duration};
582    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
583    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
584    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
585    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
586    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
587    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
588    /// # runtime.block_on(async {
589    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
590    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
591    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
592    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
593    /// # let provider = Provider::from_config(ProviderConfig::new(
594    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
595    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
596    /// # let outcome = async {
597    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
598    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
599    /// # let cwd = std::env::current_dir()?;
600    /// # let context = WorkspaceContext::new(cwd.clone(),
601    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
602    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
603    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
604    /// let id = workspace.id();
605    /// workspace.close().await?;
606    /// let reopened = agent.get_workspace(id.clone()).await?;
607    /// assert_eq!(reopened.id(), id);
608    /// reopened.close().await?;
609    /// # Ok::<(), Box<dyn std::error::Error>>(())
610    /// # }.await;
611    /// # let cleanup = agent.shutdown().await;
612    /// # outcome?;
613    /// # cleanup?;
614    /// # Ok::<(), Box<dyn std::error::Error>>(())
615    /// # })?;
616    /// # Ok::<(), Box<dyn std::error::Error>>(())
617    /// ```
618    fn get_workspace(
619        &self,
620        id: WorkspaceId,
621    ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_;
622
623    /// Performs a read-only advisory preflight for a workspace replacement.
624    ///
625    /// A completed `Ready` reserves no later mutation right. The durable expected
626    /// revision is checked before dynamic readiness; the preflight excludes its
627    /// own transient admission from the activity it reports.
628    ///
629    /// # Errors
630    ///
631    /// Returns [`KitError`] for invalid or unsupported replacement data, a
632    /// missing/foreign workspace, stale revision, closed runtime, or an owning
633    /// state/dependency/deadline failure. Dynamic activity is reported as
634    /// [`WorkspaceUpdateReadiness::Busy`].
635    ///
636    /// # Examples
637    ///
638    /// This local Custom agent uses in-memory Lance and sends no network requests.
639    ///
640    /// ```
641    /// # use std::{num::NonZeroUsize, time::Duration};
642    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
643    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
644    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
645    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
646    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
647    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
648    /// # runtime.block_on(async {
649    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
650    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
651    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
652    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
653    /// # let provider = Provider::from_config(ProviderConfig::new(
654    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
655    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
656    /// # let outcome = async {
657    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
658    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
659    /// # let cwd = std::env::current_dir()?;
660    /// # let context = WorkspaceContext::new(cwd.clone(),
661    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
662    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
663    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
664    /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceUpdateReadiness};
665    /// let current = workspace.info().await?;
666    /// let request = UpdateWorkspaceRequest::new(current.id().clone(), current.revision(),
667    ///     "renamed", current.context().clone(), current.defaults().clone())?;
668    /// assert_eq!(agent.check_workspace_update(&request).await?, WorkspaceUpdateReadiness::Ready);
669    /// // Advisory readiness does not change or reserve the durable revision.
670    /// assert_eq!(agent.workspace_info(current.id().clone()).await?.revision(), current.revision());
671    /// workspace.close().await?;
672    /// # Ok::<(), Box<dyn std::error::Error>>(())
673    /// # }.await;
674    /// # let cleanup = agent.shutdown().await;
675    /// # outcome?;
676    /// # cleanup?;
677    /// # Ok::<(), Box<dyn std::error::Error>>(())
678    /// # })?;
679    /// # Ok::<(), Box<dyn std::error::Error>>(())
680    /// ```
681    fn check_workspace_update(
682        &self,
683        request: &UpdateWorkspaceRequest,
684    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_;
685
686    /// Atomically applies a revision-checked portable workspace replacement.
687    ///
688    /// This is a full replacement of name, context, and defaults, not a patch.
689    /// The operation rechecks revision and activity atomically, preserves the
690    /// backend attachment, increments the revision once, and invalidates prior
691    /// workspace/conversation handles only after durable commit. Commit is success;
692    /// postcommit cleanup cannot change it into an error.
693    ///
694    /// # Errors
695    ///
696    /// Returns [`KitError`] for stale revision, busy or ambiguous descendant work,
697    /// missing/foreign identity, unsupported replacement policy, closed runtime,
698    /// or precommit dependency/deadline failures. Revision overflow is
699    /// [`crate::error::ErrorKind::InvalidState`] and does not change state.
700    ///
701    /// # Examples
702    ///
703    /// This local Custom agent uses in-memory Lance and sends no network requests.
704    ///
705    /// ```
706    /// # use std::{num::NonZeroUsize, time::Duration};
707    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
708    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
709    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
710    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
711    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
712    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
713    /// # runtime.block_on(async {
714    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
715    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
716    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
717    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
718    /// # let provider = Provider::from_config(ProviderConfig::new(
719    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
720    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
721    /// # let outcome = async {
722    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
723    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
724    /// # let cwd = std::env::current_dir()?;
725    /// # let context = WorkspaceContext::new(cwd.clone(),
726    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
727    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
728    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
729    /// use agents_kit::workspace::UpdateWorkspaceRequest;
730    /// let current = workspace.info().await?;
731    /// workspace.close().await?;
732    /// let replacement = UpdateWorkspaceRequest::new(current.id().clone(), current.revision(),
733    ///     "renamed", current.context().clone(), current.defaults().clone())?;
734    /// let updated = agent.update_workspace(replacement).await?;
735    /// assert_eq!(updated.name(), "renamed");
736    /// assert_eq!(updated.revision(), current.revision() + 1);
737    /// # Ok::<(), Box<dyn std::error::Error>>(())
738    /// # }.await;
739    /// # let cleanup = agent.shutdown().await;
740    /// # outcome?;
741    /// # cleanup?;
742    /// # Ok::<(), Box<dyn std::error::Error>>(())
743    /// # })?;
744    /// # Ok::<(), Box<dyn std::error::Error>>(())
745    /// ```
746    fn update_workspace(
747        &self,
748        request: UpdateWorkspaceRequest,
749    ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
750
751    /// Deletes a workspace after validating its expected revision.
752    ///
753    /// Requires quiescent descendants and the current revision; there is no force
754    /// mode. Durable removal of workspace/child inventory is the success boundary.
755    /// Postcommit native cleanup remains a recovery obligation and cannot reverse
756    /// success.
757    ///
758    /// # Errors
759    ///
760    /// Returns [`KitError`] for stale revision, active or ambiguous descendants,
761    /// missing/foreign identity, closed runtime, or precommit state/dependency/
762    /// deadline failure.
763    ///
764    /// # Examples
765    ///
766    /// This local Custom agent uses in-memory Lance and sends no network requests.
767    ///
768    /// ```
769    /// # use std::{num::NonZeroUsize, time::Duration};
770    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
771    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
772    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
773    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
774    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
775    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
776    /// # runtime.block_on(async {
777    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
778    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
779    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
780    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
781    /// # let provider = Provider::from_config(ProviderConfig::new(
782    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
783    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
784    /// # let outcome = async {
785    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
786    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
787    /// # let cwd = std::env::current_dir()?;
788    /// # let context = WorkspaceContext::new(cwd.clone(),
789    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
790    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
791    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
792    /// use agents_kit::workspace::DeleteWorkspaceRequest;
793    /// let current = workspace.info().await?;
794    /// workspace.close().await?;
795    /// agent.delete_workspace(DeleteWorkspaceRequest::new(current.id().clone(), current.revision())?).await?;
796    /// assert!(agent.list_workspaces().await?.is_empty());
797    /// # Ok::<(), Box<dyn std::error::Error>>(())
798    /// # }.await;
799    /// # let cleanup = agent.shutdown().await;
800    /// # outcome?;
801    /// # cleanup?;
802    /// # Ok::<(), Box<dyn std::error::Error>>(())
803    /// # })?;
804    /// # Ok::<(), Box<dyn std::error::Error>>(())
805    /// ```
806    fn delete_workspace(
807        &self,
808        request: DeleteWorkspaceRequest,
809    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
810
811    /// Lists selectable models in stable canonical-selector order.
812    ///
813    /// The complete selectable inventory retains canonical selectors and supported
814    /// reasoning/personality values. Listing does not reserve future availability;
815    /// selection is revalidated in the eventual workspace/conversation context.
816    ///
817    /// # Errors
818    ///
819    /// Returns [`KitError`] when the inventory route is unsupported or incomplete,
820    /// selected external data is malformed, the runtime is closed, or a dependency
821    /// operation fails or times out. An unavailable inventory is not an empty success.
822    ///
823    /// # Examples
824    ///
825    /// This example requires a live Agent with its configured backend and storage
826    /// integrations; it is compiled without starting those services.
827    ///
828    /// ```no_run
829    /// use agents_kit::{AgentBackend, KitError};
830    /// use agents_kit::model::ModelInfo;
831    ///
832    /// async fn available_models<A: AgentBackend>(agent: &A) -> Result<Vec<ModelInfo>, KitError> {
833    ///     agent.list_models().await
834    /// }
835    /// ```
836    fn list_models(&self) -> impl Future<Output = Result<Vec<ModelInfo>, KitError>> + Send + '_;
837
838    /// Replaces the agent-scoped managed-knowledge source set.
839    ///
840    /// An omitted scope selects Agent scope; an explicit Database scope follows
841    /// the managed-knowledge contract. Nonempty ingestion replaces the selected source
842    /// set atomically; an empty request is a no-op after scope validation, not deletion.
843    /// It does not execute a prompt or create tools. See [`crate::knowledge`] for a
844    /// complete local ingestion workflow.
845    ///
846    /// # Errors
847    ///
848    /// Returns [`KitError`] for invalid source/scope data, missing or incompatible
849    /// knowledge integrations, closed or busy admission, dependency failures, or
850    /// timeout. Unsupported scope or backend semantics fail explicitly.
851    ///
852    /// # Examples
853    ///
854    /// This example requires a live Agent with its configured backend and storage
855    /// integrations; it is compiled without starting those services.
856    ///
857    /// ```no_run
858    /// use agents_kit::{AgentBackend, KitError};
859    /// use agents_kit::knowledge::IngestKnowledgeRequest;
860    ///
861    /// async fn replace_sources<A: AgentBackend>(agent: &A, request: IngestKnowledgeRequest)
862    ///     -> Result<(), KitError>
863    /// {
864    ///     agent.ingest_knowledge(request).await
865    /// }
866    /// ```
867    fn ingest_knowledge(
868        &self,
869        request: IngestKnowledgeRequest,
870    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
871
872    /// Consumes the implementation, seals new work, and settles owned cleanup.
873    ///
874    /// Live descendants are invalidated after accepted work and cleanup settle.
875    /// Quiescent durable workspaces and conversations remain available to a new
876    /// parent runtime. Dropping an Agent provides no asynchronous cleanup guarantee.
877    ///
878    /// # Errors
879    ///
880    /// Returns [`KitError`] if consuming cleanup cannot fully settle within the
881    /// lifecycle deadline or an owned resource fails cleanup. The owner is not
882    /// restored; preserve the primary error and its supplementary cleanup facts
883    /// and use durable state for later reconciliation.
884    ///
885    /// # Examples
886    ///
887    /// This local Custom agent uses in-memory Lance and sends no network requests.
888    ///
889    /// ```
890    /// # use std::{num::NonZeroUsize, time::Duration};
891    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
892    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
893    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
894    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
895    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
896    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
897    /// # runtime.block_on(async {
898    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
899    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
900    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
901    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
902    /// # let provider = Provider::from_config(ProviderConfig::new(
903    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
904    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
905    /// assert!(agent.list_workspaces().await?.is_empty());
906    /// // Consuming shutdown joins owned cleanup before this runtime is dropped.
907    /// agent.shutdown().await?;
908    /// # Ok::<(), Box<dyn std::error::Error>>(())
909    /// # })?;
910    /// # Ok::<(), Box<dyn std::error::Error>>(())
911    /// ```
912    fn shutdown(self) -> impl Future<Output = Result<(), KitError>> + Send
913    where
914        Self: Sized;
915}
916
917/// A live handle to one durable agent and its workspace inventory.
918///
919/// Construct or reopen it with [`crate::AgentBuilder`] and import
920/// [`AgentBackend`] for behavior. The handle is unique and non-Clone; its
921/// workspace type preserves the same concrete storage/provider/tool types.
922/// Explicit shutdown consumes it and settles owned work. Drop alone offers no
923/// asynchronous cleanup guarantee.
924///
925/// # Examples
926///
927/// This local Custom agent uses in-memory Lance and sends no network requests.
928///
929/// ```
930/// use std::{num::NonZeroUsize, time::Duration};
931/// use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
932/// use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
933/// use agents_kit::{model::ModelSelector, tool::ToolSelection};
934/// use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
935/// use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
936/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
937/// # runtime.block_on(async {
938/// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
939/// let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
940///     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
941/// let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
942/// let provider = Provider::from_config(ProviderConfig::new(
943///     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
944/// let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
945/// # let outcome = async {
946/// let identity = agent.id();
947/// let observed = agent.info().await;
948/// assert_eq!(observed?.id(), &identity);
949/// # Ok::<(), Box<dyn std::error::Error>>(())
950/// # }.await;
951/// # let cleanup = agent.shutdown().await;
952/// # outcome?;
953/// # cleanup?;
954/// # Ok::<(), Box<dyn std::error::Error>>(())
955/// # })?;
956/// # Ok::<(), Box<dyn std::error::Error>>(())
957/// ```
958pub struct Agent<
959    S: StateStoreBackend = storage_kit::StateStore,
960    D: DatabaseBackend = storage_kit::Database,
961    P: ProviderBackend = providers_kit::Provider,
962    T: ToolsetBackend = tools_kit::Toolset,
963> {
964    inner: AgentInner<S, D, P, T>,
965}
966
967enum AgentInner<S: StateStoreBackend, D: DatabaseBackend, P: ProviderBackend, T: ToolsetBackend> {
968    Codex(Box<codex::agent::CodexAgent<S, D, T>>),
969    OpenCode(Box<opencode::agent::OpenCodeAgent<S, D>>),
970    Custom(Box<custom::CustomAgent<S, D, P, T>>),
971}
972
973impl<S, D, P, T> Agent<S, D, P, T>
974where
975    S: StateStoreBackend,
976    D: DatabaseBackend,
977    P: ProviderBackend,
978    T: ToolsetBackend,
979{
980    pub(crate) fn codex(backend: codex::agent::CodexAgent<S, D, T>) -> Self {
981        Self {
982            inner: AgentInner::Codex(Box::new(backend)),
983        }
984    }
985
986    pub(crate) fn opencode(backend: opencode::agent::OpenCodeAgent<S, D>) -> Self {
987        Self {
988            inner: AgentInner::OpenCode(Box::new(backend)),
989        }
990    }
991
992    pub(crate) fn custom(backend: custom::CustomAgent<S, D, P, T>) -> Self {
993        Self {
994            inner: AgentInner::Custom(Box::new(backend)),
995        }
996    }
997
998    pub(crate) fn retain_storage_claim(&mut self) {
999        match &mut self.inner {
1000            AgentInner::Codex(backend) => backend.retain_storage_claim(),
1001            AgentInner::OpenCode(backend) => backend.retain_storage_claim(),
1002            AgentInner::Custom(backend) => backend.retain_storage_claim(),
1003        }
1004    }
1005}
1006
1007impl<S, D, P, T> AgentBackend for AgentInner<S, D, P, T>
1008where
1009    S: StateStoreBackend,
1010    D: DatabaseBackend,
1011    P: ProviderBackend,
1012    T: ToolsetBackend,
1013{
1014    type Workspace = Workspace<S, D, P, T>;
1015
1016    fn id(&self) -> AgentId {
1017        match self {
1018            AgentInner::Codex(backend) => backend.id(),
1019            AgentInner::OpenCode(backend) => backend.id(),
1020            AgentInner::Custom(backend) => backend.id(),
1021        }
1022    }
1023
1024    async fn info(&self) -> Result<AgentInfo, KitError> {
1025        match self {
1026            AgentInner::Codex(backend) => backend.info().await,
1027            AgentInner::OpenCode(backend) => backend.info().await,
1028            AgentInner::Custom(backend) => backend.info().await,
1029        }
1030    }
1031
1032    async fn new_workspace(
1033        &self,
1034        request: NewWorkspace,
1035    ) -> Result<Workspace<S, D, P, T>, KitError> {
1036        match self {
1037            AgentInner::Codex(backend) => {
1038                backend.new_workspace(request).await.map(Workspace::codex)
1039            }
1040            AgentInner::OpenCode(backend) => backend
1041                .new_workspace(request)
1042                .await
1043                .map(Workspace::opencode),
1044            AgentInner::Custom(backend) => {
1045                backend.new_workspace(request).await.map(Workspace::custom)
1046            }
1047        }
1048    }
1049
1050    async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, KitError> {
1051        match self {
1052            AgentInner::Codex(backend) => backend.list_workspaces().await,
1053            AgentInner::OpenCode(backend) => backend.list_workspaces().await,
1054            AgentInner::Custom(backend) => backend.list_workspaces().await,
1055        }
1056    }
1057
1058    async fn workspace_info(&self, id: WorkspaceId) -> Result<WorkspaceInfo, KitError> {
1059        match self {
1060            AgentInner::Codex(backend) => backend.workspace_info(id).await,
1061            AgentInner::OpenCode(backend) => backend.workspace_info(id).await,
1062            AgentInner::Custom(backend) => backend.workspace_info(id).await,
1063        }
1064    }
1065
1066    async fn get_workspace(&self, id: WorkspaceId) -> Result<Workspace<S, D, P, T>, KitError> {
1067        match self {
1068            AgentInner::Codex(backend) => backend.get_workspace(id).await.map(Workspace::codex),
1069            AgentInner::OpenCode(backend) => {
1070                backend.get_workspace(id).await.map(Workspace::opencode)
1071            }
1072            AgentInner::Custom(backend) => backend.get_workspace(id).await.map(Workspace::custom),
1073        }
1074    }
1075
1076    fn check_workspace_update(
1077        &self,
1078        request: &UpdateWorkspaceRequest,
1079    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_ {
1080        let request = request.clone();
1081        async move {
1082            match self {
1083                AgentInner::Codex(backend) => backend.check_workspace_update(&request).await,
1084                AgentInner::OpenCode(backend) => backend.check_workspace_update(&request).await,
1085                AgentInner::Custom(backend) => backend.check_workspace_update(&request).await,
1086            }
1087        }
1088    }
1089
1090    async fn update_workspace(
1091        &self,
1092        request: UpdateWorkspaceRequest,
1093    ) -> Result<WorkspaceInfo, KitError> {
1094        match self {
1095            AgentInner::Codex(backend) => backend.update_workspace(request).await,
1096            AgentInner::OpenCode(backend) => backend.update_workspace(request).await,
1097            AgentInner::Custom(backend) => backend.update_workspace(request).await,
1098        }
1099    }
1100
1101    async fn delete_workspace(&self, request: DeleteWorkspaceRequest) -> Result<(), KitError> {
1102        match self {
1103            AgentInner::Codex(backend) => backend.delete_workspace(request).await,
1104            AgentInner::OpenCode(backend) => backend.delete_workspace(request).await,
1105            AgentInner::Custom(backend) => backend.delete_workspace(request).await,
1106        }
1107    }
1108
1109    async fn list_models(&self) -> Result<Vec<ModelInfo>, KitError> {
1110        match self {
1111            AgentInner::Codex(backend) => backend.list_models().await,
1112            AgentInner::OpenCode(backend) => backend.list_models().await,
1113            AgentInner::Custom(backend) => backend.list_models().await,
1114        }
1115    }
1116
1117    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
1118        match self {
1119            AgentInner::Codex(backend) => backend.ingest_knowledge(request).await,
1120            AgentInner::OpenCode(backend) => backend.ingest_knowledge(request).await,
1121            AgentInner::Custom(backend) => backend.ingest_knowledge(request).await,
1122        }
1123    }
1124
1125    async fn shutdown(self) -> Result<(), KitError> {
1126        match self {
1127            AgentInner::Codex(backend) => (*backend).shutdown().await,
1128            AgentInner::OpenCode(backend) => (*backend).shutdown().await,
1129            AgentInner::Custom(backend) => (*backend).shutdown().await,
1130        }
1131    }
1132}
1133
1134impl<S, D, P, T> AgentBackend for Agent<S, D, P, T>
1135where
1136    S: StateStoreBackend,
1137    D: DatabaseBackend,
1138    P: ProviderBackend,
1139    T: ToolsetBackend,
1140{
1141    type Workspace = Workspace<S, D, P, T>;
1142
1143    fn id(&self) -> AgentId {
1144        self.inner.id()
1145    }
1146
1147    async fn info(&self) -> Result<AgentInfo, KitError> {
1148        self.inner.info().await.map_err(|error| {
1149            error
1150                .at_operation(crate::error::Operation::AgentInfo)
1151                .in_phase(crate::error::ErrorPhase::StateRead)
1152        })
1153    }
1154
1155    async fn new_workspace(
1156        &self,
1157        request: NewWorkspace,
1158    ) -> Result<Workspace<S, D, P, T>, KitError> {
1159        self.inner.new_workspace(request).await.map_err(|error| {
1160            error
1161                .at_operation(crate::error::Operation::NewWorkspace)
1162                .in_phase(crate::error::ErrorPhase::Admission)
1163        })
1164    }
1165
1166    async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, KitError> {
1167        self.inner.list_workspaces().await.map_err(|error| {
1168            error
1169                .at_operation(crate::error::Operation::ListWorkspaces)
1170                .in_phase(crate::error::ErrorPhase::StateRead)
1171        })
1172    }
1173
1174    async fn workspace_info(&self, id: WorkspaceId) -> Result<WorkspaceInfo, KitError> {
1175        self.inner.workspace_info(id).await.map_err(|error| {
1176            error
1177                .at_operation(crate::error::Operation::WorkspaceInfo)
1178                .in_phase(crate::error::ErrorPhase::StateRead)
1179        })
1180    }
1181
1182    async fn get_workspace(&self, id: WorkspaceId) -> Result<Workspace<S, D, P, T>, KitError> {
1183        self.inner.get_workspace(id).await.map_err(|error| {
1184            error
1185                .at_operation(crate::error::Operation::GetWorkspace)
1186                .in_phase(crate::error::ErrorPhase::Admission)
1187        })
1188    }
1189
1190    fn check_workspace_update(
1191        &self,
1192        request: &UpdateWorkspaceRequest,
1193    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_ {
1194        let check = self.inner.check_workspace_update(request);
1195        async move {
1196            check.await.map_err(|error| {
1197                error
1198                    .at_operation(crate::error::Operation::CheckWorkspaceUpdate)
1199                    .in_phase(crate::error::ErrorPhase::InputValidation)
1200            })
1201        }
1202    }
1203
1204    async fn update_workspace(
1205        &self,
1206        request: UpdateWorkspaceRequest,
1207    ) -> Result<WorkspaceInfo, KitError> {
1208        self.inner.update_workspace(request).await.map_err(|error| {
1209            error
1210                .at_operation(crate::error::Operation::UpdateWorkspace)
1211                .in_phase(crate::error::ErrorPhase::Admission)
1212        })
1213    }
1214
1215    async fn delete_workspace(&self, request: DeleteWorkspaceRequest) -> Result<(), KitError> {
1216        self.inner.delete_workspace(request).await.map_err(|error| {
1217            error
1218                .at_operation(crate::error::Operation::DeleteWorkspace)
1219                .in_phase(crate::error::ErrorPhase::Admission)
1220        })
1221    }
1222
1223    async fn list_models(&self) -> Result<Vec<ModelInfo>, KitError> {
1224        self.inner.list_models().await.map_err(|error| {
1225            error
1226                .at_operation(crate::error::Operation::ListModels)
1227                .in_phase(crate::error::ErrorPhase::InventoryVerification)
1228        })
1229    }
1230
1231    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
1232        self.inner.ingest_knowledge(request).await.map_err(|error| {
1233            error
1234                .at_operation(crate::error::Operation::IngestKnowledge)
1235                .in_phase(crate::error::ErrorPhase::KnowledgeWrite)
1236        })
1237    }
1238
1239    async fn shutdown(self) -> Result<(), KitError> {
1240        self.inner.shutdown().await.map_err(|error| {
1241            error
1242                .at_operation(crate::error::Operation::Shutdown)
1243                .in_phase(crate::error::ErrorPhase::Lifecycle)
1244        })
1245    }
1246}