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}