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/// whose associated Conversation remains concrete. Runtime selection may use an
100/// exhaustive caller-owned enum hierarchy; no normalization constructor or
101/// independent child type parameter is needed.
102///
103/// Accepted work must reach its documented durable terminal state even if a caller
104/// drops the awaiting future. Shutdown seals new work, invalidates live descendants,
105/// and settles accepted cleanup.
106///
107/// Start with the [local construction example](crate) and [`Self::new_workspace`].
108/// For a complete caller-owned hierarchy, the [executable direct-trait examples](https://github.com/jecsand838/ai-kit/blob/main/agents-kit/tests/mock_hierarchy/mod.rs)
109/// cover Agent, associated Workspace and Conversation, exact requests, lifecycle
110/// and errors. Their process-local state is not native durability evidence.
111/// The [Custom composition tests](https://github.com/jecsand838/ai-kit/blob/main/agents-kit/tests/custom_mock_composition.rs)
112/// exercise the real Custom runtime with caller-owned lower implementations.
113pub trait AgentBackend: Send + Sync + 'static {
114    /// Concrete workspace implementation produced by this agent.
115    type Workspace: crate::WorkspaceBackend;
116
117    /// Returns the durable portable agent identity.
118    ///
119    /// The identity remains stable across runtime reopen. This value identifies
120    /// the durable Agent; it does not expose backend routing or lower resources.
121    ///
122    /// See the [local construction example](crate) and [`Self::new_workspace`] for child ownership.
123    fn id(&self) -> AgentId;
124
125    /// Loads the current descriptive agent information.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`KitError`] if the runtime is closed, committed state cannot be
130    /// read, or stored facts contradict the portable record contract.
131    ///
132    /// See the [local construction example](crate) and [`Self::new_workspace`] for child ownership.
133    fn info(&self) -> impl Future<Output = Result<AgentInfo, KitError>> + Send + '_;
134
135    /// Creates and publishes a workspace from validated portable configuration.
136    ///
137    /// Publishes a handle only after complete context/defaults, backend attachment,
138    /// and durable ownership agree. A new workspace begins at revision one.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`KitError`] for closed or busy admission, unsupported execution
143    /// policy/integration composition, invalid input, dependency failure, or
144    /// deadline expiry. Accepted native setup must remain durably attached or
145    /// recovery-owned when an operation fails.
146    ///
147    /// # Examples
148    ///
149    /// This local Custom agent uses in-memory Lance and sends no network requests.
150    ///
151    /// ```
152    /// # use std::{num::NonZeroUsize, time::Duration};
153    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
154    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
155    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
156    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
157    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
158    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
159    /// # runtime.block_on(async {
160    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
161    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
162    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
163    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
164    /// # let provider = Provider::from_config(ProviderConfig::new(
165    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
166    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
167    /// # let outcome = async {
168    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
169    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
170    /// # let cwd = std::env::current_dir()?;
171    /// # let context = WorkspaceContext::new(cwd.clone(),
172    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
173    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
174    /// let workspace = agent.new_workspace(
175    ///     NewWorkspace::new("review", context, WorkspaceDefaults::empty())?,
176    /// ).await?;
177    /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceUpdateReadiness, DeleteWorkspaceRequest};
178    /// let observed = async {
179    ///     let current = workspace.info().await?;
180    ///     assert_eq!(current.revision(), 1);
181    ///     let request = UpdateWorkspaceRequest::new(current.id().clone(), current.revision(),
182    ///         "renamed", current.context().clone(), current.defaults().clone())?;
183    ///     // Preflight observes the still-open workspace runtime.
184    ///     assert_eq!(agent.check_workspace_update(&request).await?, WorkspaceUpdateReadiness::Ready);
185    ///     // Readiness is advisory: it does not reserve or change the revision.
186    ///     assert_eq!(agent.workspace_info(current.id().clone()).await?.revision(), current.revision());
187    ///     Ok::<_, agents_kit::KitError>((current, request))
188    /// }.await;
189    /// let close = workspace.close().await;
190    /// let (current, request) = match (observed, close) {
191    ///     (Ok(value), Ok(())) => Ok(value),
192    ///     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
193    ///     (Err(error), _) | (_, Err(error)) => Err(error),
194    /// }?;
195    /// // Revision-checked mutations also accept a quiescent, closed workspace.
196    /// let updated = agent.update_workspace(request).await?;
197    /// assert_eq!(updated.name(), "renamed");
198    /// assert_eq!(updated.revision(), current.revision() + 1);
199    /// agent.delete_workspace(DeleteWorkspaceRequest::new(updated.id().clone(), updated.revision())?).await?;
200    /// assert!(agent.list_workspaces().await?.is_empty());
201    /// # Ok::<(), agents_kit::KitError>(())
202    /// # }.await;
203    /// # let cleanup = agent.shutdown().await;
204    /// # match (outcome, cleanup) {
205    /// #     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
206    /// #     (Err(error), _) | (_, Err(error)) => Err(error),
207    /// #     (Ok(()), Ok(())) => Ok(()),
208    /// # }?;
209    /// # Ok::<(), Box<dyn std::error::Error>>(())
210    /// # })?;
211    /// # Ok::<(), Box<dyn std::error::Error>>(())
212    /// ```
213    fn new_workspace(
214        &self,
215        request: NewWorkspace,
216    ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_;
217
218    /// Lists the agent's durable workspaces.
219    ///
220    /// Inventory comes from committed records, not only currently opened handles.
221    /// The result does not claim live ownership of those workspaces.
222    ///
223    /// # Errors
224    ///
225    /// Returns [`KitError`] for a closed runtime, state-read failure, deadline
226    /// expiry, or inconsistent durable inventory.
227    ///
228    /// See the [local construction example](crate) and [`Self::new_workspace`] for child ownership.
229    fn list_workspaces(
230        &self,
231    ) -> impl Future<Output = Result<Vec<WorkspaceInfo>, KitError>> + Send + '_;
232
233    /// Loads descriptive information for a workspace identity.
234    ///
235    /// This reads committed metadata without claiming a live workspace handle.
236    /// Missing and foreign identities are both reported without revealing owners.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`crate::error::ErrorKind::NotFound`] for a missing or foreign id,
241    /// or the owning lifecycle, state-read, integrity, or deadline error.
242    ///
243    /// See the [local construction example](crate) and [`Self::new_workspace`] for child ownership.
244    fn workspace_info(
245        &self,
246        id: WorkspaceId,
247    ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
248
249    /// Reopens a durable workspace as a live handle.
250    ///
251    /// At most one live handle exists for a current workspace revision. Share it
252    /// by reference; a second current-revision lookup is busy. Reopen verifies
253    /// durable attachment and integration compatibility rather than inventing a
254    /// replacement workspace.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`KitError`] for a missing/foreign id, an existing live claim,
259    /// closed runtime, incompatible reopen state, dependency failure, or timeout.
260    ///
261    /// See [`WorkspaceBackend::new_conversation`](crate::WorkspaceBackend::new_conversation)
262    /// for an inventory and reopen workflow,
263    /// including release of the first workspace claim before lookup.
264    fn get_workspace(
265        &self,
266        id: WorkspaceId,
267    ) -> impl Future<Output = Result<Self::Workspace, KitError>> + Send + '_;
268
269    /// Performs a read-only advisory preflight for a workspace replacement.
270    ///
271    /// A completed `Ready` reserves no later mutation right. The durable expected
272    /// revision is checked before dynamic readiness; the preflight excludes its
273    /// own transient admission from the activity it reports.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`KitError`] for invalid or unsupported replacement data, a
278    /// missing/foreign workspace, stale revision, closed runtime, or an owning
279    /// state/dependency/deadline failure. Dynamic activity is reported as
280    /// [`WorkspaceUpdateReadiness::Busy`].
281    ///
282    /// See [`Self::new_workspace`] for a complete creation, revision-check, replacement and deletion workflow.
283    fn check_workspace_update(
284        &self,
285        request: &UpdateWorkspaceRequest,
286    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_;
287
288    /// Atomically applies a revision-checked portable workspace replacement.
289    ///
290    /// This is a full replacement of name, context, and defaults, not a patch.
291    /// The operation rechecks revision and activity atomically, preserves the
292    /// backend attachment, increments the revision once, and invalidates prior
293    /// workspace/conversation handles only after durable commit. Commit is success;
294    /// postcommit cleanup cannot change it into an error.
295    ///
296    /// # Errors
297    ///
298    /// Returns [`KitError`] for stale revision, busy or ambiguous descendant work,
299    /// missing/foreign identity, unsupported replacement policy, closed runtime,
300    /// or precommit dependency/deadline failures. Revision overflow is
301    /// [`crate::error::ErrorKind::InvalidState`] and does not change state.
302    ///
303    /// See [`Self::new_workspace`] for a complete creation, revision-check, replacement and deletion workflow.
304    fn update_workspace(
305        &self,
306        request: UpdateWorkspaceRequest,
307    ) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
308
309    /// Deletes a workspace after validating its expected revision.
310    ///
311    /// Requires quiescent descendants and the current revision; there is no force
312    /// mode. Durable removal of workspace/child inventory is the success boundary.
313    /// Postcommit native cleanup remains a recovery obligation and cannot reverse
314    /// success.
315    ///
316    /// # Errors
317    ///
318    /// Returns [`KitError`] for stale revision, active or ambiguous descendants,
319    /// missing/foreign identity, closed runtime, or precommit state/dependency/
320    /// deadline failure.
321    ///
322    /// See [`Self::new_workspace`] for a complete creation, revision-check, replacement and deletion workflow.
323    fn delete_workspace(
324        &self,
325        request: DeleteWorkspaceRequest,
326    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
327
328    /// Lists selectable models in stable canonical-selector order.
329    ///
330    /// The complete selectable inventory retains canonical selectors and supported
331    /// reasoning/personality values. Listing does not reserve future availability;
332    /// selection is revalidated in the eventual workspace/conversation context.
333    ///
334    /// # Errors
335    ///
336    /// Returns [`KitError`] when the inventory route is unsupported or incomplete,
337    /// selected external data is malformed, the runtime is closed, or a dependency
338    /// operation fails or times out. An unavailable inventory is not an empty success.
339    ///
340    /// Caller implementations need no service. The [executable caller hierarchy](https://github.com/jecsand838/ai-kit/blob/main/agents-kit/tests/mock_hierarchy/mod.rs) exercises this contract with direct trait mocks; supplied backends retain their own prerequisites.
341    fn list_models(&self) -> impl Future<Output = Result<Vec<ModelInfo>, KitError>> + Send + '_;
342
343    /// Replaces the agent-scoped managed-knowledge source set.
344    ///
345    /// An omitted scope selects Agent scope; an explicit Database scope follows
346    /// the managed-knowledge contract. Nonempty ingestion replaces the selected source
347    /// set atomically; an empty request is a no-op after scope validation, not deletion.
348    /// It does not execute a prompt or create tools. See [`crate::knowledge`] for a
349    /// complete local ingestion workflow.
350    ///
351    /// # Errors
352    ///
353    /// Returns [`KitError`] for invalid source/scope data, missing or incompatible
354    /// knowledge integrations, closed or busy admission, dependency failures, or
355    /// timeout. Unsupported scope or backend semantics fail explicitly.
356    ///
357    /// Caller implementations need no service. The [executable caller hierarchy](https://github.com/jecsand838/ai-kit/blob/main/agents-kit/tests/mock_hierarchy/mod.rs) exercises this contract with direct trait mocks; supplied backends retain their own prerequisites.
358    fn ingest_knowledge(
359        &self,
360        request: IngestKnowledgeRequest,
361    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
362
363    /// Consumes the implementation, seals new work, and settles owned cleanup.
364    ///
365    /// Live descendants are invalidated after accepted work and cleanup settle.
366    /// Quiescent durable workspaces and conversations remain available to a new
367    /// parent runtime. Dropping an Agent provides no asynchronous cleanup guarantee.
368    ///
369    /// # Errors
370    ///
371    /// Returns [`KitError`] if consuming cleanup cannot fully settle within the
372    /// lifecycle deadline or an owned resource fails cleanup. The owner is not
373    /// restored; preserve the primary error and its supplementary cleanup facts
374    /// and use durable state for later reconciliation.
375    ///
376    /// See the [local construction example](crate) and [`Self::new_workspace`] for child ownership.
377    fn shutdown(self) -> impl Future<Output = Result<(), KitError>> + Send
378    where
379        Self: Sized;
380}
381
382/// A live handle to one durable agent and its workspace inventory.
383///
384/// Construct or reopen it with [`crate::AgentBuilder`] and import
385/// [`AgentBackend`] for behavior. The handle is unique and non-Clone; its
386/// workspace type preserves the same concrete storage/provider/tool types.
387/// Explicit shutdown consumes it and settles owned work. Drop alone offers no
388/// asynchronous cleanup guarantee.
389///
390/// See [`AgentBackend`] and the [local construction example](crate) for usage.
391pub struct Agent<
392    S: StateStoreBackend = storage_kit::StateStore,
393    D: DatabaseBackend = storage_kit::Database,
394    P: ProviderBackend = providers_kit::Provider,
395    T: ToolsetBackend = tools_kit::Toolset,
396> {
397    inner: AgentInner<S, D, P, T>,
398}
399
400enum AgentInner<S: StateStoreBackend, D: DatabaseBackend, P: ProviderBackend, T: ToolsetBackend> {
401    Codex(Box<codex::agent::CodexAgent<S, D, T>>),
402    OpenCode(Box<opencode::agent::OpenCodeAgent<S, D>>),
403    Custom(Box<custom::CustomAgent<S, D, P, T>>),
404}
405
406impl<S, D, P, T> Agent<S, D, P, T>
407where
408    S: StateStoreBackend,
409    D: DatabaseBackend,
410    P: ProviderBackend,
411    T: ToolsetBackend,
412{
413    pub(crate) fn codex(backend: codex::agent::CodexAgent<S, D, T>) -> Self {
414        Self {
415            inner: AgentInner::Codex(Box::new(backend)),
416        }
417    }
418
419    pub(crate) fn opencode(backend: opencode::agent::OpenCodeAgent<S, D>) -> Self {
420        Self {
421            inner: AgentInner::OpenCode(Box::new(backend)),
422        }
423    }
424
425    pub(crate) fn custom(backend: custom::CustomAgent<S, D, P, T>) -> Self {
426        Self {
427            inner: AgentInner::Custom(Box::new(backend)),
428        }
429    }
430
431    pub(crate) fn retain_storage_claim(&mut self) {
432        match &mut self.inner {
433            AgentInner::Codex(backend) => backend.retain_storage_claim(),
434            AgentInner::OpenCode(backend) => backend.retain_storage_claim(),
435            AgentInner::Custom(backend) => backend.retain_storage_claim(),
436        }
437    }
438}
439
440impl<S, D, P, T> AgentBackend for AgentInner<S, D, P, T>
441where
442    S: StateStoreBackend,
443    D: DatabaseBackend,
444    P: ProviderBackend,
445    T: ToolsetBackend,
446{
447    type Workspace = Workspace<S, D, P, T>;
448
449    fn id(&self) -> AgentId {
450        match self {
451            AgentInner::Codex(backend) => backend.id(),
452            AgentInner::OpenCode(backend) => backend.id(),
453            AgentInner::Custom(backend) => backend.id(),
454        }
455    }
456
457    async fn info(&self) -> Result<AgentInfo, KitError> {
458        match self {
459            AgentInner::Codex(backend) => backend.info().await,
460            AgentInner::OpenCode(backend) => backend.info().await,
461            AgentInner::Custom(backend) => backend.info().await,
462        }
463    }
464
465    async fn new_workspace(
466        &self,
467        request: NewWorkspace,
468    ) -> Result<Workspace<S, D, P, T>, KitError> {
469        match self {
470            AgentInner::Codex(backend) => {
471                backend.new_workspace(request).await.map(Workspace::codex)
472            }
473            AgentInner::OpenCode(backend) => backend
474                .new_workspace(request)
475                .await
476                .map(Workspace::opencode),
477            AgentInner::Custom(backend) => {
478                backend.new_workspace(request).await.map(Workspace::custom)
479            }
480        }
481    }
482
483    async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, KitError> {
484        match self {
485            AgentInner::Codex(backend) => backend.list_workspaces().await,
486            AgentInner::OpenCode(backend) => backend.list_workspaces().await,
487            AgentInner::Custom(backend) => backend.list_workspaces().await,
488        }
489    }
490
491    async fn workspace_info(&self, id: WorkspaceId) -> Result<WorkspaceInfo, KitError> {
492        match self {
493            AgentInner::Codex(backend) => backend.workspace_info(id).await,
494            AgentInner::OpenCode(backend) => backend.workspace_info(id).await,
495            AgentInner::Custom(backend) => backend.workspace_info(id).await,
496        }
497    }
498
499    async fn get_workspace(&self, id: WorkspaceId) -> Result<Workspace<S, D, P, T>, KitError> {
500        match self {
501            AgentInner::Codex(backend) => backend.get_workspace(id).await.map(Workspace::codex),
502            AgentInner::OpenCode(backend) => {
503                backend.get_workspace(id).await.map(Workspace::opencode)
504            }
505            AgentInner::Custom(backend) => backend.get_workspace(id).await.map(Workspace::custom),
506        }
507    }
508
509    fn check_workspace_update(
510        &self,
511        request: &UpdateWorkspaceRequest,
512    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_ {
513        let request = request.clone();
514        async move {
515            match self {
516                AgentInner::Codex(backend) => backend.check_workspace_update(&request).await,
517                AgentInner::OpenCode(backend) => backend.check_workspace_update(&request).await,
518                AgentInner::Custom(backend) => backend.check_workspace_update(&request).await,
519            }
520        }
521    }
522
523    async fn update_workspace(
524        &self,
525        request: UpdateWorkspaceRequest,
526    ) -> Result<WorkspaceInfo, KitError> {
527        match self {
528            AgentInner::Codex(backend) => backend.update_workspace(request).await,
529            AgentInner::OpenCode(backend) => backend.update_workspace(request).await,
530            AgentInner::Custom(backend) => backend.update_workspace(request).await,
531        }
532    }
533
534    async fn delete_workspace(&self, request: DeleteWorkspaceRequest) -> Result<(), KitError> {
535        match self {
536            AgentInner::Codex(backend) => backend.delete_workspace(request).await,
537            AgentInner::OpenCode(backend) => backend.delete_workspace(request).await,
538            AgentInner::Custom(backend) => backend.delete_workspace(request).await,
539        }
540    }
541
542    async fn list_models(&self) -> Result<Vec<ModelInfo>, KitError> {
543        match self {
544            AgentInner::Codex(backend) => backend.list_models().await,
545            AgentInner::OpenCode(backend) => backend.list_models().await,
546            AgentInner::Custom(backend) => backend.list_models().await,
547        }
548    }
549
550    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
551        match self {
552            AgentInner::Codex(backend) => backend.ingest_knowledge(request).await,
553            AgentInner::OpenCode(backend) => backend.ingest_knowledge(request).await,
554            AgentInner::Custom(backend) => backend.ingest_knowledge(request).await,
555        }
556    }
557
558    async fn shutdown(self) -> Result<(), KitError> {
559        match self {
560            AgentInner::Codex(backend) => (*backend).shutdown().await,
561            AgentInner::OpenCode(backend) => (*backend).shutdown().await,
562            AgentInner::Custom(backend) => (*backend).shutdown().await,
563        }
564    }
565}
566
567impl<S, D, P, T> AgentBackend for Agent<S, D, P, T>
568where
569    S: StateStoreBackend,
570    D: DatabaseBackend,
571    P: ProviderBackend,
572    T: ToolsetBackend,
573{
574    type Workspace = Workspace<S, D, P, T>;
575
576    fn id(&self) -> AgentId {
577        self.inner.id()
578    }
579
580    async fn info(&self) -> Result<AgentInfo, KitError> {
581        self.inner.info().await.map_err(|error| {
582            error
583                .at_operation(crate::error::Operation::AgentInfo)
584                .in_phase(crate::error::ErrorPhase::StateRead)
585        })
586    }
587
588    async fn new_workspace(
589        &self,
590        request: NewWorkspace,
591    ) -> Result<Workspace<S, D, P, T>, KitError> {
592        self.inner.new_workspace(request).await.map_err(|error| {
593            error
594                .at_operation(crate::error::Operation::NewWorkspace)
595                .in_phase(crate::error::ErrorPhase::Admission)
596        })
597    }
598
599    async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, KitError> {
600        self.inner.list_workspaces().await.map_err(|error| {
601            error
602                .at_operation(crate::error::Operation::ListWorkspaces)
603                .in_phase(crate::error::ErrorPhase::StateRead)
604        })
605    }
606
607    async fn workspace_info(&self, id: WorkspaceId) -> Result<WorkspaceInfo, KitError> {
608        self.inner.workspace_info(id).await.map_err(|error| {
609            error
610                .at_operation(crate::error::Operation::WorkspaceInfo)
611                .in_phase(crate::error::ErrorPhase::StateRead)
612        })
613    }
614
615    async fn get_workspace(&self, id: WorkspaceId) -> Result<Workspace<S, D, P, T>, KitError> {
616        self.inner.get_workspace(id).await.map_err(|error| {
617            error
618                .at_operation(crate::error::Operation::GetWorkspace)
619                .in_phase(crate::error::ErrorPhase::Admission)
620        })
621    }
622
623    fn check_workspace_update(
624        &self,
625        request: &UpdateWorkspaceRequest,
626    ) -> impl Future<Output = Result<WorkspaceUpdateReadiness, KitError>> + Send + '_ {
627        let check = self.inner.check_workspace_update(request);
628        async move {
629            check.await.map_err(|error| {
630                error
631                    .at_operation(crate::error::Operation::CheckWorkspaceUpdate)
632                    .in_phase(crate::error::ErrorPhase::InputValidation)
633            })
634        }
635    }
636
637    async fn update_workspace(
638        &self,
639        request: UpdateWorkspaceRequest,
640    ) -> Result<WorkspaceInfo, KitError> {
641        self.inner.update_workspace(request).await.map_err(|error| {
642            error
643                .at_operation(crate::error::Operation::UpdateWorkspace)
644                .in_phase(crate::error::ErrorPhase::Admission)
645        })
646    }
647
648    async fn delete_workspace(&self, request: DeleteWorkspaceRequest) -> Result<(), KitError> {
649        self.inner.delete_workspace(request).await.map_err(|error| {
650            error
651                .at_operation(crate::error::Operation::DeleteWorkspace)
652                .in_phase(crate::error::ErrorPhase::Admission)
653        })
654    }
655
656    async fn list_models(&self) -> Result<Vec<ModelInfo>, KitError> {
657        self.inner.list_models().await.map_err(|error| {
658            error
659                .at_operation(crate::error::Operation::ListModels)
660                .in_phase(crate::error::ErrorPhase::InventoryVerification)
661        })
662    }
663
664    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
665        self.inner.ingest_knowledge(request).await.map_err(|error| {
666            error
667                .at_operation(crate::error::Operation::IngestKnowledge)
668                .in_phase(crate::error::ErrorPhase::KnowledgeWrite)
669        })
670    }
671
672    async fn shutdown(self) -> Result<(), KitError> {
673        self.inner.shutdown().await.map_err(|error| {
674            error
675                .at_operation(crate::error::Operation::Shutdown)
676                .in_phase(crate::error::ErrorPhase::Lifecycle)
677        })
678    }
679}