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}