Skip to main content

app_kit/
lib.rs

1//! Durable applications over caller-owned storage and configured agents.
2//!
3//! Start with [`AppConfig`] and [`App::open`], create a [`Workspace`], then select
4//! a [`workflow::Workflow`]. The [`App`] example runs a local state workflow with
5//! caller-created embedded Lance StateStore and no Agent value or external service.
6//!
7//! [`workflow`] contains definitions, execution controls, state operations,
8//! graph inspection, immutable checkpoints, and the five concrete App behavior
9//! seams. [`AgentSpec`] transfers an already-built Agent; App never constructs
10//! providers, tools or lower storage from declarations. Exact associated lower
11//! Workspace, Conversation and StateTransaction types remain attached.
12//!
13//! Call [`App::shutdown`] while the captured Tokio runtime and lower runtimes
14//! are alive and driven. Run handles observe accepted work; [`workflow::Execution`]
15//! owns lazy advancement. Dropping an execution can revoke uncommitted work,
16//! while observation loss cannot revoke an admitted native commit.
17//!
18//! Public supporting types live under [`workflow`]. The state serialization
19//! registry stores only field/tag codecs; it is not a runtime behavior registry.
20
21//!
22//! # Choose a workflow
23//!
24//! - Application builders: the [local workflow](#local-workflow),
25//!   [explicit execution and retained history](workflow), then
26//!   [durable reopen and migration prerequisites](App::open).
27//! - Custom implementers: [configure concrete behavior](AppConfig#configure-behavior)
28//!   with the five workflow traits. Their examples run locally; lower
29//!   [`agents_kit::AgentBackend`] and [`storage_kit::StateStoreBackend`]
30//!   implementations remain direct, statically selected values.
31//! - Agent-backed workflows: transfer already-built values through [`AgentSpec`].
32//!   App owns eventual consuming Agent shutdown, without rebuilding credentials,
33//!   tools, Providers or persistence from declarations.
34//!
35//! # Local workflow
36//!
37//! ```
38//! use agents_kit::Agent;
39//! use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
40//! use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
41//!     RunStatus, WorkflowDefinition, WorkflowState};
42//! use storage_kit::StateStore;
43//! use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
44//!
45//! let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
46//! runtime.block_on(async {
47//! let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
48//! let definition = WorkflowDefinition::parse(r#"{
49//!     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
50//!     "state":{"count":0}, "required":["count"],
51//!     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
52//!     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
53//!         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
54//!     ]}}], "routes":{}
55//! }"#)?;
56//! let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
57//! let mut app = App::open(config).await?;
58//! let mut workspace = app.create_workspace(WorkspaceConfig {
59//!     name: Some("local counter".into()), root: std::env::current_dir()?,
60//! }).await?;
61//! let workflow = workspace.workflow("counter".parse()?).await?;
62//! let run = workflow.start(ExecutionOptions::default()).await?;
63//! let snapshot = run.wait().await?;
64//! assert!(matches!(snapshot.status, RunStatus::Succeeded));
65//! assert_eq!(snapshot.state.values()["count"], 1);
66//! #
67//! app.shutdown(Shutdown::Drain { timeout: std::time::Duration::from_secs(5) }).await?;
68//! # Ok::<(), Box<dyn std::error::Error>>(())
69//! })?;
70//! # Ok::<(), Box<dyn std::error::Error>>(())
71//! ```
72
73#![forbid(unsafe_code)]
74#![warn(missing_docs)]
75
76use std::fmt;
77use std::num::NonZeroUsize;
78use std::path::PathBuf;
79use std::str::FromStr;
80use std::sync::Arc;
81use std::time::Duration;
82
83use agents_kit::AgentBackend;
84use serde::{Deserialize, Deserializer, Serialize, Serializer};
85use storage_kit::StateStoreBackend;
86use uuid::{Uuid, Variant, Version};
87
88use crate::workflow::{
89    ActionBackend, ChildExecutor, ConditionBackend, InlineExecutor, JsonState, LifecycleHooks,
90    NoActions, NoConditions, NoHooks, StateTyping, Workflow, WorkflowDefinition, WorkflowId,
91};
92
93mod app;
94mod storage;
95mod workspace;
96
97/// Opaque workflow definitions and runtime handles.
98pub mod workflow;
99
100/// An open application generation.
101///
102/// Owns one durable application generation, its transferred agents and accepted work.
103///
104/// Supply a concrete [`storage_kit::StateStoreBackend`] through [`AppConfig`].
105/// Each [`AgentSpec`] preserves its concrete [`agents_kit::AgentBackend`] and its
106/// associated Workspace/Conversation types. An empty agent inventory can execute
107/// state-only workflows; the explicit `Agent` type below constructs no Agent value.
108///
109/// Keep the Tokio runtime and any caller-owned lower runtimes alive and driven
110/// through [`Self::shutdown`]. Root drop revokes admission and requests supervised
111/// best-effort cleanup; it cannot substitute for awaited shutdown. Workspace and
112/// run observers do not keep admission alive after the App is revoked.
113///
114/// # Example
115///
116/// ```
117/// use agents_kit::Agent;
118/// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
119/// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
120///     RunStatus, WorkflowDefinition, WorkflowState};
121/// use storage_kit::StateStore;
122/// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
123///
124/// let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
125/// runtime.block_on(async {
126/// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
127/// let definition = WorkflowDefinition::parse(r#"{
128///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
129///     "state":{"count":0}, "required":["count"],
130///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
131///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
132///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
133///     ]}}], "routes":{}
134/// }"#)?;
135/// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
136/// let mut app = App::open(config).await?;
137/// let mut workspace = app.create_workspace(WorkspaceConfig {
138///     name: Some("local counter".into()), root: std::env::current_dir()?,
139/// }).await?;
140/// let workflow = workspace.workflow("counter".parse()?).await?;
141/// let run = workflow.start(ExecutionOptions::default()).await?;
142/// let snapshot = run.wait().await?;
143/// assert!(matches!(snapshot.status, RunStatus::Succeeded));
144/// assert_eq!(snapshot.state.values()["count"], 1);
145/// #
146/// app.shutdown(Shutdown::Drain { timeout: std::time::Duration::from_secs(5) }).await?;
147/// # Ok::<(), Box<dyn std::error::Error>>(())
148/// })?;
149/// # Ok::<(), Box<dyn std::error::Error>>(())
150/// ```
151pub struct App<
152    CS: StateStoreBackend,
153    A: AgentBackend,
154    X: ActionBackend = NoActions,
155    C: ConditionBackend = NoConditions,
156    T: StateTyping = JsonState,
157    H: LifecycleHooks = NoHooks,
158    E: ChildExecutor = InlineExecutor,
159> {
160    runtime: Arc<app::AppRuntime<CS, A, X, C, T, H, E>>,
161}
162
163/// Complete application configuration.
164///
165/// App does not offer a separate built-in storage construction path:
166///
167/// ```compile_fail
168/// let _ = app_kit::AppState::<storage_kit::StateStore>::Ephemeral;
169/// ```
170///
171/// Owns caller-built storage, a homogeneous agent inventory, and workflow definitions.
172///
173/// The consuming `with_*` methods change their concrete App behavior parameter.
174/// They do not erase backends or alter already admitted runs. Heterogeneous agent
175/// inventories use a caller-owned exhaustive hierarchy. Dropping configuration
176/// before open does not perform asynchronous Agent shutdown.
177///
178/// # Configure behavior
179///
180/// Start with ready storage, ready Agents, and parsed definitions. Apply the
181/// consuming configuration methods before [`App::open`]; they retain exact
182/// implementation types, including associated lower children and transaction
183/// ownership. None registers a runtime callback or erases a backend.
184///
185/// | Behavior | Complete implementation example | Attach before open |
186/// | --- | --- | --- |
187/// | Produce lazy action events and writes | [`workflow::ActionBackend`] | [`Self::with_actions`] |
188/// | Evaluate declared routing predicates | [`workflow::ConditionBackend`] | [`Self::with_conditions`] |
189/// | Construct and validate application state | [`workflow::StateTyping`] | [`Self::with_state_typing`] |
190/// | Observe authoritative lifecycle events | [`workflow::LifecycleHooks`] | [`Self::with_hooks`] |
191/// | Drive concrete local child futures | [`workflow::ChildExecutor`] | [`Self::with_child_executor`] |
192///
193/// The `with_*` examples exercise the complete path from a concrete implementation
194/// to opening App. Action and condition declarations must match the workflow's
195/// exact logical keys and revisions. A custom child executor must return every
196/// admitted completion exactly once; it never creates child identities. App's
197/// built-in `NoActions`, `NoConditions`, `JsonState`, `NoHooks` and `InlineExecutor`
198/// are suitable defaults only where their actual behavior satisfies the workflow.
199///
200/// Lower composition is independent: `A::Workspace` fixes its Conversation,
201/// while `CS::Transaction` fixes consuming commit/rollback. For heterogeneous
202/// Agent inventories implement matching caller-owned exhaustive hierarchies;
203/// do not substitute public trait objects or independent child parameters.
204///
205/// # Example
206///
207/// ```
208/// # use agents_kit::Agent;
209/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
210/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
211/// #     RunStatus, WorkflowDefinition, WorkflowState};
212/// # use storage_kit::StateStore;
213/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
214/// #
215/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
216/// # runtime.block_on(async {
217/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
218/// # let definition = WorkflowDefinition::parse(r#"{
219/// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
220/// #     "state":{"count":0}, "required":["count"],
221/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
222/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
223/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
224/// #     ]}}], "routes":{}
225/// # }"#)?;
226/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
227/// let mut app = App::open(config).await?;
228/// app.shutdown(Shutdown::RejectActive).await?;
229/// #
230/// # Ok::<(), Box<dyn std::error::Error>>(())
231/// # })?;
232/// # Ok::<(), Box<dyn std::error::Error>>(())
233/// ```
234pub struct AppConfig<
235    CS: StateStoreBackend,
236    A: AgentBackend,
237    X: ActionBackend = NoActions,
238    C: ConditionBackend = NoConditions,
239    T: StateTyping = JsonState,
240    H: LifecycleHooks = NoHooks,
241    E: ChildExecutor = InlineExecutor,
242> {
243    state: CS,
244    agents: Vec<AgentSpec<A>>,
245    workflows: Vec<WorkflowDefinition>,
246    actions: Vec<X>,
247    conditions: Vec<C>,
248    typing: T,
249    hooks: H,
250    executor: E,
251}
252
253/// Declares one configured lower agent.
254///
255/// The key and revision are caller-owned logical binding metadata, validated by
256/// [`App::open`]. Keep the same revision only while the configuration is compatible.
257/// App never derives compatibility from the opaque lower Agent identity. A
258/// successful open takes responsibility for consuming Agent shutdown.
259///
260/// # Example
261///
262/// This example requires an already-running OpenCode service at the shown endpoint.
263///
264/// ```no_run
265/// use std::{num::NonZeroUsize, time::Duration};
266/// use agents_kit::{Agent, AgentBuilder, AgentStorage};
267/// use agents_kit::config::{AgentConfig, OpenCodeConfig};
268/// use app_kit::{AgentSpec, App, AppConfig, Shutdown};
269/// use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
270/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
271/// # runtime.block_on(async {
272/// let agent_store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
273/// let backend = OpenCodeConfig::external("http://127.0.0.1:4096", Duration::from_secs(10),
274///     Duration::from_secs(60), Duration::from_secs(10))?;
275/// let agent_config = AgentConfig::new(backend.into(), Duration::from_secs(10),
276///     NonZeroUsize::new(1).unwrap())?;
277/// let agent: Agent = AgentBuilder::create(agent_config, AgentStorage::new(agent_store)).build().await?;
278/// let spec = AgentSpec::new("reviewer".into(), "r1".into(), agent);
279/// let app_store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
280/// let mut app = App::open(AppConfig::new(app_store, vec![spec], Vec::new())).await?;
281/// // App now owns consuming shutdown of the transferred Agent.
282/// app.shutdown(Shutdown::Drain { timeout: Duration::from_secs(30) }).await?;
283/// # Ok::<(), Box<dyn std::error::Error>>(())
284/// # })?;
285/// # Ok::<(), Box<dyn std::error::Error>>(())
286/// ```
287pub struct AgentSpec<A: AgentBackend> {
288    key: String,
289    revision: String,
290    agent: Arc<A>,
291}
292
293/// Selects application shutdown behavior.
294///
295/// Drain timeouts limit observation only. Once admission is sealed, a timed-out
296/// or dropped shutdown observer does not cancel the finalizer or reopen App.
297/// Keep all owning runtimes driven until shutdown completes.
298pub enum Shutdown {
299    /// Rejects shutdown while accepted work remains.
300    RejectActive,
301    /// Seals admission and drains accepted work.
302    Drain {
303        /// Bounds this caller's observation of finalization.
304        timeout: Duration,
305    },
306    /// Seals admission, cancels accepted work, and drains it.
307    CancelAndDrain {
308        /// Bounds this caller's observation of finalization.
309        timeout: Duration,
310    },
311}
312
313/// An application workspace handle.
314///
315/// A typed projection into the owning App generation. Dropping it does not delete
316/// or close lower workspaces. Replacement uses a complete configuration and a
317/// revision fence; successful deletion revokes this projection. Retained terminal
318/// history can still be opened through its workflow after workspace deletion.
319///
320/// # Example
321///
322/// ```
323/// # use agents_kit::Agent;
324/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
325/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
326/// #     RunStatus, WorkflowDefinition, WorkflowState};
327/// # use storage_kit::StateStore;
328/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
329/// #
330/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
331/// # runtime.block_on(async {
332/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
333/// # let definition = WorkflowDefinition::parse(r#"{
334/// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
335/// #     "state":{"count":0}, "required":["count"],
336/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
337/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
338/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
339/// #     ]}}], "routes":{}
340/// # }"#)?;
341/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
342/// # let mut app = App::open(config).await?;
343/// # let mut workspace = app.create_workspace(WorkspaceConfig {
344/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
345/// # }).await?;
346/// let snapshot = workspace.snapshot().await?;
347/// assert_eq!(&snapshot.id, workspace.id());
348/// assert_eq!(snapshot.revision, 1);
349/// #
350/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
351/// # Ok::<(), Box<dyn std::error::Error>>(())
352/// # })?;
353/// # Ok::<(), Box<dyn std::error::Error>>(())
354/// ```
355pub struct Workspace<
356    CS: StateStoreBackend,
357    A: AgentBackend,
358    X: ActionBackend = NoActions,
359    C: ConditionBackend = NoConditions,
360    T: StateTyping = JsonState,
361    H: LifecycleHooks = NoHooks,
362    E: ChildExecutor = InlineExecutor,
363> {
364    runtime: workspace::WorkspaceRuntime<CS, A, X, C, T, H, E>,
365}
366
367/// A validated application workspace identity.
368///
369/// Parsing and deserialization require `appws_` followed by a canonical lowercase,
370/// hyphenated UUIDv4. Identity equality does not establish lower-agent compatibility.
371///
372/// # Example
373///
374/// ```
375/// use app_kit::WorkspaceId;
376/// let id: WorkspaceId = "appws_8a7c251e-b5b8-4d9e-8f0b-123456789abc".parse()?;
377/// assert_eq!(id.to_string(), "appws_8a7c251e-b5b8-4d9e-8f0b-123456789abc");
378/// assert!("appws_not-a-uuid".parse::<WorkspaceId>().is_err());
379/// # Ok::<(), app_kit::AppError>(())
380/// ```
381#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
382pub struct WorkspaceId(String);
383
384/// Complete caller-authored workspace configuration.
385///
386/// The root is an absolute, lexical UTF-8 path, 1–4,096 bytes without control
387/// characters. Validation does not create directories or resolve symlinks. A present
388/// name is trimmed and must be nonempty, control-free, and at most 256 UTF-8 bytes.
389///
390/// # Example
391///
392/// ```
393/// use app_kit::WorkspaceConfig;
394/// let config = WorkspaceConfig { name: Some("review".into()), root: std::env::current_dir()? };
395/// assert!(config.root.is_absolute());
396/// # Ok::<(), std::io::Error>(())
397/// ```
398pub struct WorkspaceConfig {
399    /// Optionally names the lower workspace.
400    pub name: Option<String>,
401    /// Selects the absolute workspace root.
402    pub root: PathBuf,
403}
404
405/// The current caller-visible workspace truth.
406///
407/// # Example
408///
409/// ```
410/// # use agents_kit::Agent;
411/// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
412/// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
413/// #     RunStatus, WorkflowDefinition, WorkflowState};
414/// # use storage_kit::StateStore;
415/// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
416/// #
417/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
418/// # runtime.block_on(async {
419/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
420/// # let definition = WorkflowDefinition::parse(r#"{
421/// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
422/// #     "state":{"count":0}, "required":["count"],
423/// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
424/// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
425/// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
426/// #     ]}}], "routes":{}
427/// # }"#)?;
428/// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
429/// # let mut app = App::open(config).await?;
430/// # let mut workspace = app.create_workspace(WorkspaceConfig {
431/// #     name: Some("local counter".into()), root: std::env::current_dir()?,
432/// # }).await?;
433/// let snapshot = workspace.snapshot().await?;
434/// let updated = workspace.replace(snapshot.revision, WorkspaceConfig {
435///     name: Some("renamed".into()), root: snapshot.config.root,
436/// }).await?;
437/// assert_eq!(updated.revision, snapshot.revision + 1);
438/// #
439/// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
440/// # Ok::<(), Box<dyn std::error::Error>>(())
441/// # })?;
442/// # Ok::<(), Box<dyn std::error::Error>>(())
443/// ```
444#[non_exhaustive]
445pub struct WorkspaceSnapshot {
446    /// Identifies the workspace.
447    pub id: WorkspaceId,
448    /// Fences complete replacement.
449    pub revision: u64,
450    /// Contains the committed configuration.
451    pub config: WorkspaceConfig,
452}
453
454/// A safe caller-visible application error.
455///
456/// Operational messages are bounded and redact lower payloads and identities.
457/// Errors supplied by explicitly registered codecs or lifecycle hooks retain
458/// caller context at that opt-in boundary. Match variants (with a fallback for
459/// this non-exhaustive enum), rather than parsing Display text. An error from an
460/// observer does not by itself prove an accepted native operation rolled back.
461#[non_exhaustive]
462#[derive(Debug)]
463pub enum AppError {
464    /// Caller-authored input is invalid.
465    InvalidInput(String),
466    /// Exact scoped evidence proves a resource absent.
467    NotFound(String),
468    /// A durable fact, fence, or transition conflicts.
469    Conflict(String),
470    /// Finite capacity or an exclusive owner prevents progress.
471    Busy(String),
472    /// Revocation or terminal invalidation rejects new work.
473    Closed(String),
474    /// An application-owned deadline elapsed.
475    Timeout(String),
476    /// Caller behavior, a lower public operation, or a private durable record failed.
477    Integration(String),
478}
479
480enum IdentitySyntax {
481    Named(&'static str),
482    Uuid {
483        name: &'static str,
484        prefix: &'static str,
485    },
486}
487
488impl<
489    CS: StateStoreBackend,
490    A: AgentBackend,
491    X: ActionBackend,
492    C: ConditionBackend,
493    T: StateTyping,
494    H: LifecycleHooks,
495    E: ChildExecutor,
496> App<CS, A, X, C, T, H, E>
497{
498    /// Opens or reconciles one application generation.
499    ///
500    /// Validates bindings and retained records, claims one durable generation, and
501    /// reconstructs recovery ownership before returning. A successful newer opener
502    /// fences handles from the previous generation. Configuration is consumed.
503    ///
504    /// # Durable reopen and deployment
505    ///
506    /// Reopen with a fresh handle to the same caller-owned StateStore, the same
507    /// compatible workflow definitions, and freshly constructed or reopened lower
508    /// integrations. Never reuse a consumed Agent or infer a new logical identity
509    /// from its display representation. The state-only example below needs no Agent
510    /// value and demonstrates that shutdown retains history until explicit deletion.
511    ///
512    /// Existing App v6 records require a **cold, one-way v6-to-v7 migration**.
513    /// Stop older clients, settle all their owners and take a whole-store physical
514    /// backup before opening with this version. Opening performs the supported
515    /// migration before claiming the new generation. Mixed-version operation,
516    /// in-place downgrade, reset and corruption repair are not supported. Restore
517    /// the physical backup for a rollback deployment; do not open migrated data
518    /// with an older client. Unsupported epochs and corrupt current records fail
519    /// rather than falling back to another decoder.
520    ///
521    /// After Agent ownership transfer, ordinary failure awaits consuming Agent cleanup
522    /// and preserves the primary failure. Cancelling this open future during transfer
523    /// does not promise graceful cleanup: reopen the durable lower products and App to
524    /// reconcile their retained facts.
525    ///
526    /// # Errors
527    ///
528    /// Returns [`AppError::InvalidInput`] for invalid or conflicting construction
529    /// metadata, [`AppError::Conflict`] for incompatible retained revisions or lost
530    /// ownership, and [`AppError::Integration`] for unsupported durable versions,
531    /// invalid retained records, or lower failures. Recovery can also report capacity,
532    /// closure, or deadline errors; no live App is returned on failure.
533    ///
534    /// # Example
535    ///
536    /// ```
537    /// use agents_kit::Agent;
538    /// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
539    /// use app_kit::workflow::{ExecutionOptions, RunSelector, RunStatus, WorkflowDefinition};
540    /// use storage_kit::StateStore;
541    /// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
542    /// use std::time::Duration;
543    /// let directory = std::env::temp_dir().join(format!("ai-kit-app-doc-{}", uuid::Uuid::new_v4()));
544    /// std::fs::create_dir(&directory)?;
545    /// let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
546    /// let result = runtime.block_on(async {
547    ///     let definition_source = r#"{
548    ///         "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
549    ///         "state":{"count":0},"required":["count"],"reducers":{"count":"reject_concurrent"},
550    ///         "entry":"increment","nodes":[{"id":"increment","step":{"kind":"state",
551    ///         "mutations":[{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}]}}],
552    ///         "routes":{}
553    ///     }"#;
554    ///     let config = StateStoreConfig::lance(LanceStateStoreConfig::directory(&directory));
555    ///     let store = StateStore::connect(config.clone()).await?;
556    ///     let mut app = App::open(AppConfig::<_, Agent>::new(store, Vec::new(), vec![WorkflowDefinition::parse(definition_source)?])).await?;
557    ///     let first = async {
558    ///         let workspace = app.create_workspace(WorkspaceConfig { name: None, root: directory.clone() }).await?;
559    ///         let workflow = workspace.workflow("counter".parse()?).await?;
560    ///         let run = workflow.start(ExecutionOptions::default()).await?;
561    ///         let terminal = run.wait().await?;
562    ///         assert!(matches!(terminal.status, RunStatus::Succeeded));
563    ///         Ok::<_, app_kit::AppError>((workspace.id().clone(), run.id().clone()))
564    ///     }.await;
565    ///     let shutdown = app.shutdown(Shutdown::CancelAndDrain { timeout: Duration::from_secs(5) }).await;
566    ///     drop(app);
567    ///     let (workspace_id, run_id) = first?;
568    ///     shutdown?;
569    ///
570    ///     let fresh_store = StateStore::connect(config).await?;
571    ///     let mut reopened = App::open(AppConfig::<_, Agent>::new(fresh_store, Vec::new(), vec![WorkflowDefinition::parse(definition_source)?])).await?;
572    ///     let readback = async {
573    ///         let workspace = reopened.workspace(workspace_id).await?;
574    ///         let workflow = workspace.workflow("counter".parse()?).await?;
575    ///         let mut run = workflow.open_run(RunSelector::Latest(run_id.clone())).await?;
576    ///         let terminal = run.snapshot().await?;
577    ///         assert!(matches!(terminal.status, RunStatus::Succeeded));
578    ///         assert_eq!(terminal.state.values()["count"], 1);
579    ///         run.delete(terminal.checkpoint).await?;
580    ///         assert!(matches!(workflow.open_run(RunSelector::Latest(run_id)).await,
581    ///             Err(app_kit::AppError::NotFound(_))));
582    ///         Ok::<(), app_kit::AppError>(())
583    ///     }.await;
584    ///     let shutdown = reopened.shutdown(Shutdown::CancelAndDrain { timeout: Duration::from_secs(5) }).await;
585    ///     drop(reopened);
586    ///     readback?;
587    ///     shutdown?;
588    ///     Ok::<(), Box<dyn std::error::Error>>(())
589    /// });
590    /// // A shutdown timeout bounds the waiter, not necessarily the owned finalizer.
591    /// // Remove the directory only after both generations demonstrably settled.
592    /// if result.is_ok() {
593    ///     std::fs::remove_dir_all(&directory)?;
594    /// } else {
595    ///     eprintln!("Retain {} for cleanup reconciliation", directory.display());
596    /// }
597    /// result?;
598    /// # Ok::<(), Box<dyn std::error::Error>>(())
599    /// ```
600    pub async fn open(config: AppConfig<CS, A, X, C, T, H, E>) -> Result<Self, AppError> {
601        Ok(Self {
602            runtime: app::AppTrace::span(|| tracing::info_span!(target: "app_kit", "app.open"))
603                .observe(std::pin::pin!(app::AppRuntime::open(config)))
604                .await?,
605        })
606    }
607
608    /// Creates and materializes one workspace.
609    ///
610    /// Materializes the complete configured Agent key/revision inventory before
611    /// publishing the App workspace. Durable intent precedes each lower operation;
612    /// unresolved partial progress remains recoverable rather than a visible success.
613    ///
614    /// # Errors
615    ///
616    /// Returns [`AppError::InvalidInput`] for invalid configuration, [`AppError::Busy`]
617    /// when admission is full, or [`AppError::Closed`] after revocation. Conflicting
618    /// materialization evidence, lower/storage failures, and bounded waits report
619    /// `Conflict`, `Integration`, or `Timeout` respectively.
620    ///
621    /// # Example
622    ///
623    /// ```
624    /// # use agents_kit::Agent;
625    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
626    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
627    /// #     RunStatus, WorkflowDefinition, WorkflowState};
628    /// # use storage_kit::StateStore;
629    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
630    /// #
631    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
632    /// # runtime.block_on(async {
633    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
634    /// # let definition = WorkflowDefinition::parse(r#"{
635    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
636    /// #     "state":{"count":0}, "required":["count"],
637    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
638    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
639    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
640    /// #     ]}}], "routes":{}
641    /// # }"#)?;
642    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
643    /// # let mut app = App::open(config).await?;
644    /// let workspace = app.create_workspace(WorkspaceConfig {
645    ///     name: Some("review".into()), root: std::env::current_dir()?,
646    /// }).await?;
647    /// assert_eq!(workspace.snapshot().await?.revision, 1);
648    /// #
649    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
650    /// # Ok::<(), Box<dyn std::error::Error>>(())
651    /// # })?;
652    /// # Ok::<(), Box<dyn std::error::Error>>(())
653    /// ```
654    pub async fn create_workspace(
655        &self,
656        config: WorkspaceConfig,
657    ) -> Result<Workspace<CS, A, X, C, T, H, E>, AppError> {
658        Ok(Workspace {
659            runtime: self.runtime.create_workspace(config).await?,
660        })
661    }
662
663    /// Reopens one workspace by typed identity.
664    ///
665    /// A retained terminal head can establish a history-only scope after coordination
666    /// is deleted. That handle permits retained-run lookup, not fresh admission.
667    ///
668    /// # Errors
669    ///
670    /// Returns [`AppError::NotFound`] when neither active coordination nor readable
671    /// retained history exists in this scope. Revocation, incompatible lifecycle
672    /// progress, and unreadable durable evidence report `Closed`, `Conflict`, or
673    /// `Integration`.
674    ///
675    /// # Example
676    ///
677    /// ```
678    /// # use agents_kit::Agent;
679    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
680    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
681    /// #     RunStatus, WorkflowDefinition, WorkflowState};
682    /// # use storage_kit::StateStore;
683    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
684    /// #
685    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
686    /// # runtime.block_on(async {
687    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
688    /// # let definition = WorkflowDefinition::parse(r#"{
689    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
690    /// #     "state":{"count":0}, "required":["count"],
691    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
692    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
693    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
694    /// #     ]}}], "routes":{}
695    /// # }"#)?;
696    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
697    /// # let mut app = App::open(config).await?;
698    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
699    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
700    /// # }).await?;
701    /// let id = workspace.id().clone();
702    /// let reopened = app.workspace(id.clone()).await?;
703    /// assert_eq!(reopened.id(), &id);
704    /// #
705    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
706    /// # Ok::<(), Box<dyn std::error::Error>>(())
707    /// # })?;
708    /// # Ok::<(), Box<dyn std::error::Error>>(())
709    /// ```
710    pub async fn workspace(
711        &self,
712        id: WorkspaceId,
713    ) -> Result<Workspace<CS, A, X, C, T, H, E>, AppError> {
714        Ok(Workspace {
715            runtime: self.runtime.workspace(id).await?,
716        })
717    }
718
719    /// Lists workspaces in canonical identity order.
720    ///
721    /// The cursor is exclusive and pages contain active workspaces only. Limits are
722    /// 1–1,000; larger requests are rejected rather than silently clamped.
723    ///
724    /// # Errors
725    ///
726    /// Returns [`AppError::InvalidInput`] for an excessive limit, [`AppError::NotFound`]
727    /// for an unknown cursor, and `Closed` or `Integration` for revoked or unreadable
728    /// application state.
729    ///
730    /// # Example
731    ///
732    /// ```
733    /// # use agents_kit::Agent;
734    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
735    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
736    /// #     RunStatus, WorkflowDefinition, WorkflowState};
737    /// # use storage_kit::StateStore;
738    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
739    /// #
740    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
741    /// # runtime.block_on(async {
742    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
743    /// # let definition = WorkflowDefinition::parse(r#"{
744    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
745    /// #     "state":{"count":0}, "required":["count"],
746    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
747    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
748    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
749    /// #     ]}}], "routes":{}
750    /// # }"#)?;
751    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
752    /// # let mut app = App::open(config).await?;
753    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
754    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
755    /// # }).await?;
756    /// let page = app.list_workspaces(None, std::num::NonZeroUsize::new(1).unwrap()).await?;
757    /// assert_eq!(&page[0].id, workspace.id());
758    /// let next = app.list_workspaces(Some(page[0].id.clone()), std::num::NonZeroUsize::new(1).unwrap()).await?;
759    /// assert!(next.is_empty());
760    /// #
761    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
762    /// # Ok::<(), Box<dyn std::error::Error>>(())
763    /// # })?;
764    /// # Ok::<(), Box<dyn std::error::Error>>(())
765    /// ```
766    pub async fn list_workspaces(
767        &self,
768        after: Option<WorkspaceId>,
769        limit: NonZeroUsize,
770    ) -> Result<Vec<WorkspaceSnapshot>, AppError> {
771        self.runtime.list_workspaces(after, limit).await
772    }
773
774    /// Finalizes this application according to `policy`.
775    ///
776    /// `RejectActive` leaves admission open if work remains. Drain policies seal
777    /// admission; `CancelAndDrain` also requests explicit cancellation. Successful
778    /// completion joins App-owned work and consumes each transferred Agent once.
779    /// The timeout bounds this observer, not the continuing finalizer. Cancelling the
780    /// observer or timing out never reopens admission or detaches accepted cleanup.
781    ///
782    /// # Errors
783    ///
784    /// Returns [`AppError::Busy`] when `RejectActive` encounters accepted work,
785    /// [`AppError::InvalidInput`] for an invalid timeout, or [`AppError::Timeout`] when
786    /// observation expires. Durable conflicts and lower cleanup failures remain
787    /// `Conflict` or `Integration`; cleanup does not erase an earlier failure.
788    ///
789    /// # Example
790    ///
791    /// ```
792    /// # use agents_kit::Agent;
793    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
794    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
795    /// #     RunStatus, WorkflowDefinition, WorkflowState};
796    /// # use storage_kit::StateStore;
797    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
798    /// #
799    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
800    /// # runtime.block_on(async {
801    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
802    /// # let definition = WorkflowDefinition::parse(r#"{
803    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
804    /// #     "state":{"count":0}, "required":["count"],
805    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
806    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
807    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
808    /// #     ]}}], "routes":{}
809    /// # }"#)?;
810    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
811    /// # let mut app = App::open(config).await?;
812    /// app.shutdown(Shutdown::Drain { timeout: std::time::Duration::from_secs(5) }).await?;
813    /// #
814    /// # Ok::<(), Box<dyn std::error::Error>>(())
815    /// # })?;
816    /// # Ok::<(), Box<dyn std::error::Error>>(())
817    /// ```
818    pub async fn shutdown(&mut self, policy: Shutdown) -> Result<(), AppError> {
819        self.runtime.shutdown(policy).await
820    }
821}
822
823impl<CS: StateStoreBackend, A: AgentBackend> AppConfig<CS, A> {
824    /// Constructs configuration from a caller-owned control-plane store and agents.
825    ///
826    /// The store is required even for process-local operation. App preserves its
827    /// concrete transaction type and never constructs lower products. Ownership
828    /// transfers to App when [`App::open`] is called.
829    ///
830    /// # Example
831    ///
832    /// ```
833    /// # use agents_kit::Agent;
834    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
835    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
836    /// #     RunStatus, WorkflowDefinition, WorkflowState};
837    /// # use storage_kit::StateStore;
838    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
839    /// #
840    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
841    /// # runtime.block_on(async {
842    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
843    /// # let definition = WorkflowDefinition::parse(r#"{
844    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
845    /// #     "state":{"count":0}, "required":["count"],
846    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
847    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
848    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
849    /// #     ]}}], "routes":{}
850    /// # }"#)?;
851    /// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
852    /// let mut app = App::open(config).await?;
853    /// app.shutdown(Shutdown::RejectActive).await?;
854    /// #
855    /// # Ok::<(), Box<dyn std::error::Error>>(())
856    /// # })?;
857    /// # Ok::<(), Box<dyn std::error::Error>>(())
858    /// ```
859    #[must_use]
860    pub fn new(state: CS, agents: Vec<AgentSpec<A>>, workflows: Vec<WorkflowDefinition>) -> Self {
861        Self {
862            state,
863            agents,
864            workflows,
865            actions: Vec::new(),
866            conditions: Vec::new(),
867            typing: JsonState,
868            hooks: NoHooks,
869            executor: InlineExecutor,
870        }
871    }
872}
873
874impl<
875    CS: StateStoreBackend,
876    A: AgentBackend,
877    X: ActionBackend,
878    C: ConditionBackend,
879    T: StateTyping,
880    H: LifecycleHooks,
881    E: ChildExecutor,
882> AppConfig<CS, A, X, C, T, H, E>
883{
884    /// Replaces the complete caller-owned action inventory before opening App.
885    ///
886    /// Values retain their concrete type and need not implement `Clone`. App
887    /// validates their stable declarations before accepting any workflow work.
888    /// Replacing an inventory drops its previous values without invoking them.
889    ///
890    /// # Example
891    ///
892    /// ```
893    /// use agents_kit::Agent;
894    /// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
895    /// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
896    ///     RunStatus, WorkflowDefinition, WorkflowState};
897    /// use storage_kit::StateStore;
898    /// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
899    /// use app_kit::{AppError, workflow::{ActionBackend, ActionDeclaration, ActionEvent,
900    ///     ActionMode, ActionRequest, StateChange}};
901    /// use serde_json::{Map, Value};
902    /// struct Increment { declaration: ActionDeclaration }
903    /// impl ActionBackend for Increment {
904    ///     type Stream = futures_util::stream::Iter<std::vec::IntoIter<Result<ActionEvent, AppError>>>;
905    ///     fn declaration(&self) -> &ActionDeclaration { &self.declaration }
906    ///     fn start(&self, request: ActionRequest) -> Self::Stream {
907    ///         let count = request.state.values()["count"].as_i64().unwrap() + 1;
908    ///         futures_util::stream::iter(vec![Ok(ActionEvent::Completed {
909    ///             result: Map::from_iter([("new_count".into(), Value::from(count))]),
910    ///             writes: vec![StateChange::Increment { values: vec![("count".into(), Value::from(1))] }],
911    ///         })])
912    ///     }
913    /// }
914    /// let action = Increment { declaration: ActionDeclaration {
915    ///     key: "increment".into(), revision: "r1".into(), reads: vec!["count".into()],
916    ///     writes: vec!["count".into()], required_inputs: Vec::new(), optional_inputs: Vec::new(),
917    ///     bound_inputs: Map::new(), default_inputs: Map::new(), tags: vec!["counter".into()],
918    ///     source: Some("increment count by one".into()), mode: ActionMode::Both,
919    /// } };
920    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
921    /// # runtime.block_on(async {
922    /// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
923    /// let definition = WorkflowDefinition::parse(r#"{
924    ///  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
925    ///  "state":{"count":0},"required":["count"],"reducers":{"count":"reject_concurrent"},
926    ///  "entry":"increment","nodes":[{"id":"increment","step":{"kind":"action",
927    ///  "action":"increment","revision":"r1"}}],"routes":{}
928    /// }"#)?;
929    /// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
930    /// let config = config.with_actions(vec![action]);
931    /// let mut app = App::open(config).await?;
932    /// let workspace = app.create_workspace(WorkspaceConfig { name: None, root: std::env::current_dir()? }).await?;
933    /// let workflow = workspace.workflow("counter".parse()?).await?;
934    /// let run = workflow.start(ExecutionOptions::default()).await?;
935    /// assert_eq!(run.wait().await?.state.values()["count"], 1);
936    /// app.shutdown(Shutdown::Drain { timeout: std::time::Duration::from_secs(5) }).await?;
937    /// #
938    /// # Ok::<(), Box<dyn std::error::Error>>(())
939    /// # })?;
940    /// # Ok::<(), Box<dyn std::error::Error>>(())
941    /// ```
942    #[must_use]
943    pub fn with_actions<N: ActionBackend>(
944        self,
945        actions: Vec<N>,
946    ) -> AppConfig<CS, A, N, C, T, H, E> {
947        AppConfig {
948            state: self.state,
949            agents: self.agents,
950            workflows: self.workflows,
951            actions,
952            conditions: self.conditions,
953            typing: self.typing,
954            hooks: self.hooks,
955            executor: self.executor,
956        }
957    }
958
959    /// Replaces the complete caller-owned condition inventory before opening App.
960    ///
961    /// App validates stable logical keys and revisions without evaluating the
962    /// predicates. Replacing an inventory drops its previous values.
963    ///
964    /// # Example
965    ///
966    /// ```
967    /// use agents_kit::Agent;
968    /// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
969    /// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
970    ///     RunStatus, WorkflowDefinition, WorkflowState};
971    /// use storage_kit::StateStore;
972    /// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
973    /// use app_kit::{AppError, workflow::{ConditionBackend, ConditionDeclaration}};
974    /// struct Ready { declaration: ConditionDeclaration }
975    /// impl ConditionBackend for Ready {
976    ///     fn declaration(&self) -> &ConditionDeclaration { &self.declaration }
977    ///     fn evaluate(&self, state: &WorkflowState) -> Result<bool, AppError> {
978    ///         let count = state.values().get("count").and_then(serde_json::Value::as_i64)
979    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))?;
980    ///         Ok(count >= 3)
981    ///     }
982    /// }
983    /// let condition = Ready { declaration: ConditionDeclaration {
984    ///     key: "ready".into(), revision: "r1".into(), reads: vec!["count".into()],
985    /// } };
986    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
987    /// # runtime.block_on(async {
988    /// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
989    /// let definition = WorkflowDefinition::parse(r#"{
990    ///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
991    ///     "state":{"count":0}, "required":["count"],
992    ///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
993    ///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
994    ///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
995    ///     ]}}], "routes":{"increment":{"mode":"first","branches":[
996    ///         {"when":{"kind":"custom","condition":"ready","revision":"r1"},"target":{"kind":"end"}},
997    ///         {"target":{"kind":"node","id":"increment"}}
998    ///     ]}}
999    /// }"#)?;
1000    /// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1001    /// let config = config.with_conditions(vec![condition]);
1002    /// let mut app = App::open(config).await?;
1003    /// let workspace = app.create_workspace(WorkspaceConfig { name: None, root: std::env::current_dir()? }).await?;
1004    /// let workflow = workspace.workflow("counter".parse()?).await?;
1005    /// let terminal = workflow.start(ExecutionOptions::default()).await?.wait().await?;
1006    /// assert!(matches!(terminal.status, RunStatus::Succeeded));
1007    /// assert_eq!(terminal.state.values()["count"], 3);
1008    /// app.shutdown(Shutdown::Drain { timeout: std::time::Duration::from_secs(5) }).await?;
1009    /// #
1010    /// # Ok::<(), Box<dyn std::error::Error>>(())
1011    /// # })?;
1012    /// # Ok::<(), Box<dyn std::error::Error>>(())
1013    /// ```
1014    #[must_use]
1015    pub fn with_conditions<N: ConditionBackend>(
1016        self,
1017        conditions: Vec<N>,
1018    ) -> AppConfig<CS, A, X, N, T, H, E> {
1019        AppConfig {
1020            state: self.state,
1021            agents: self.agents,
1022            workflows: self.workflows,
1023            actions: self.actions,
1024            conditions,
1025            typing: self.typing,
1026            hooks: self.hooks,
1027            executor: self.executor,
1028        }
1029    }
1030
1031    /// Replaces the caller-owned workflow typing policy before opening App.
1032    ///
1033    /// The policy remains concrete and need not implement `Clone`. Its data
1034    /// values are local to validation or execution and need not be `Send`.
1035    ///
1036    /// # Example
1037    ///
1038    /// ```
1039    /// use agents_kit::Agent;
1040    /// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1041    /// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1042    ///     RunStatus, WorkflowDefinition, WorkflowState};
1043    /// use storage_kit::StateStore;
1044    /// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1045    /// use app_kit::{AppError, workflow::{StateTyping, StateChange}};
1046    /// use serde_json::{Map, Value};
1047    /// struct Counter;
1048    /// impl StateTyping for Counter {
1049    ///     type Data = i64;
1050    ///     fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
1051    ///         state.values().get("count").and_then(Value::as_i64)
1052    ///             .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
1053    ///     }
1054    ///     fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
1055    ///         WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
1056    ///     }
1057    ///     fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
1058    ///         if let StateChange::Increment { values } = change {
1059    ///             if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
1060    ///                 return Err(AppError::InvalidInput("count cannot decrease".into()));
1061    ///             }
1062    ///         }
1063    ///         Ok(())
1064    ///     }
1065    ///     fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
1066    ///         if self.construct_data(after)? < self.construct_data(before)? {
1067    ///             return Err(AppError::InvalidInput("count cannot decrease".into()));
1068    ///         }
1069    ///         Ok(())
1070    ///     }
1071    ///     fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
1072    ///         if self.construct_data(state)? < 0 {
1073    ///             return Err(AppError::InvalidInput("count must be nonnegative".into()));
1074    ///         }
1075    ///         Ok(())
1076    ///     }
1077    /// }
1078    /// #
1079    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1080    /// # runtime.block_on(async {
1081    /// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1082    /// let definition = WorkflowDefinition::parse(r#"{
1083    ///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1084    ///     "state":{"count":0}, "required":["count"],
1085    ///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1086    ///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1087    ///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1088    ///     ]}}], "routes":{}
1089    /// }"#)?;
1090    /// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1091    /// let config = config.with_state_typing(Counter);
1092    /// let mut app = App::open(config).await?;
1093    /// let workspace = app.create_workspace(WorkspaceConfig { name: None, root: std::env::current_dir()? }).await?;
1094    /// let workflow = workspace.workflow("counter".parse()?).await?;
1095    /// let invalid = WorkflowState::new(serde_json::from_str(r#"{"count":-1}"#)?)?;
1096    /// assert!(workflow.prepare(invalid).await.is_err());
1097    /// app.shutdown(Shutdown::RejectActive).await?;
1098    /// #
1099    /// # Ok::<(), Box<dyn std::error::Error>>(())
1100    /// # })?;
1101    /// # Ok::<(), Box<dyn std::error::Error>>(())
1102    /// ```
1103    #[must_use]
1104    pub fn with_state_typing<N: StateTyping>(self, typing: N) -> AppConfig<CS, A, X, C, N, H, E> {
1105        AppConfig {
1106            state: self.state,
1107            agents: self.agents,
1108            workflows: self.workflows,
1109            actions: self.actions,
1110            conditions: self.conditions,
1111            typing,
1112            hooks: self.hooks,
1113            executor: self.executor,
1114        }
1115    }
1116
1117    /// Replaces the caller-owned lifecycle observer before opening App.
1118    ///
1119    /// Replacement drops the prior observer without invoking it. The new
1120    /// observer retains its concrete type and need not implement `Clone`.
1121    ///
1122    /// # Example
1123    ///
1124    /// ```
1125    /// use agents_kit::Agent;
1126    /// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1127    /// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1128    ///     RunStatus, WorkflowDefinition, WorkflowState};
1129    /// use storage_kit::StateStore;
1130    /// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1131    /// use app_kit::{AppError, workflow::{LifecycleEvent, LifecycleHooks}};
1132    /// use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
1133    /// struct CountEvents(Arc<AtomicUsize>);
1134    /// impl LifecycleHooks for CountEvents {
1135    ///     fn observe(&self, event: &LifecycleEvent<'_>) -> Result<(), AppError> {
1136    ///         if matches!(event, LifecycleEvent::ExecuteStarted { .. }) {
1137    ///             self.0.fetch_add(1, Ordering::SeqCst);
1138    ///         }
1139    ///         Ok(())
1140    ///     }
1141    ///     fn observe_async<'a>(&'a self, event: &'a LifecycleEvent<'a>)
1142    ///         -> impl std::future::Future<Output = Result<(), AppError>> + Send + 'a {
1143    ///         async move {
1144    ///             if matches!(event, LifecycleEvent::ExecuteStarted { .. }) {
1145    ///                 self.0.fetch_add(10, Ordering::SeqCst);
1146    ///             }
1147    ///             Ok(())
1148    ///         }
1149    ///     }
1150    /// }
1151    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1152    /// # runtime.block_on(async {
1153    /// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1154    /// let definition = WorkflowDefinition::parse(r#"{
1155    ///     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1156    ///     "state":{"count":0}, "required":["count"],
1157    ///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1158    ///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1159    ///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1160    ///     ]}}], "routes":{}
1161    /// }"#)?;
1162    /// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1163    /// let count = Arc::new(AtomicUsize::new(0));
1164    /// let config = config.with_hooks(CountEvents(Arc::clone(&count)));
1165    /// let mut app = App::open(config).await?;
1166    /// let workspace = app.create_workspace(WorkspaceConfig { name: None, root: std::env::current_dir()? }).await?;
1167    /// let workflow = workspace.workflow("counter".parse()?).await?;
1168    /// let mut run = workflow.prepare(WorkflowState::default()).await?;
1169    /// run.execution(ExecutionOptions::default()).await?.finish().await?;
1170    /// assert_eq!(count.load(Ordering::SeqCst), 11);
1171    /// app.shutdown(Shutdown::Drain { timeout: std::time::Duration::from_secs(5) }).await?;
1172    /// #
1173    /// # Ok::<(), Box<dyn std::error::Error>>(())
1174    /// # })?;
1175    /// # Ok::<(), Box<dyn std::error::Error>>(())
1176    /// ```
1177    #[must_use]
1178    pub fn with_hooks<N: LifecycleHooks>(self, hooks: N) -> AppConfig<CS, A, X, C, T, N, E> {
1179        AppConfig {
1180            state: self.state,
1181            agents: self.agents,
1182            workflows: self.workflows,
1183            actions: self.actions,
1184            conditions: self.conditions,
1185            typing: self.typing,
1186            hooks,
1187            executor: self.executor,
1188        }
1189    }
1190
1191    /// Replaces the caller-owned admitted child executor before opening App.
1192    ///
1193    /// The executor remains concrete. Its associated execution futures are
1194    /// created only for admitted child work and need not implement `Send`.
1195    ///
1196    /// # Example
1197    ///
1198    /// ```
1199    /// use agents_kit::Agent;
1200    /// use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1201    /// use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1202    ///     RunStatus, WorkflowDefinition, WorkflowState};
1203    /// use storage_kit::StateStore;
1204    /// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1205    /// #
1206    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1207    /// # runtime.block_on(async {
1208    /// let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1209    /// let definition = WorkflowDefinition::parse(r#"{
1210    ///  "schema":"app-kit.workflow/v2","id":"counter","revision":"r1",
1211    ///  "state":{"sum":0},"required":["sum"],"reducers":{"sum":"sum_integer"},
1212    ///  "entry":"child","nodes":[{"id":"child","step":{"kind":"child","workflow":"child",
1213    ///  "input":{"count":{"kind":"literal","value":4}},
1214    ///  "reductions":[{"source":"count","target":"sum","reducer":"sum_integer"}]}}],"routes":{}
1215    /// }"#)?;
1216    /// let child = WorkflowDefinition::parse(r#"{
1217    ///     "schema":"app-kit.workflow/v2", "id":"child", "revision":"r1",
1218    ///     "state":{"count":0}, "required":["count"],
1219    ///     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1220    ///     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1221    ///         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1222    ///     ]}}], "routes":{}
1223    /// }"#)?;
1224    /// let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition, child])
1225    ///     .with_child_executor(app_kit::workflow::InlineExecutor);
1226    /// let mut app = App::open(config).await?;
1227    /// let mut workspace = app.create_workspace(WorkspaceConfig {
1228    ///     name: Some("local counter".into()), root: std::env::current_dir()?,
1229    /// }).await?;
1230    /// let workflow = workspace.workflow("counter".parse()?).await?;
1231    /// let mut run = workflow.prepare(WorkflowState::default()).await?;
1232    /// let terminal = run.execution(ExecutionOptions::default()).await?.finish().await?;
1233    /// assert_eq!(terminal.state.values()["sum"], 5);
1234    /// #
1235    /// app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1236    /// # Ok::<(), Box<dyn std::error::Error>>(())
1237    /// # })?;
1238    /// # Ok::<(), Box<dyn std::error::Error>>(())
1239    /// ```
1240    #[must_use]
1241    pub fn with_child_executor<N: ChildExecutor>(
1242        self,
1243        executor: N,
1244    ) -> AppConfig<CS, A, X, C, T, H, N> {
1245        AppConfig {
1246            state: self.state,
1247            agents: self.agents,
1248            workflows: self.workflows,
1249            actions: self.actions,
1250            conditions: self.conditions,
1251            typing: self.typing,
1252            hooks: self.hooks,
1253            executor,
1254        }
1255    }
1256}
1257
1258impl<A: AgentBackend> AgentSpec<A> {
1259    /// Constructs one configured lower-agent entry.
1260    ///
1261    /// The key and revision are caller-owned logical binding metadata, validated by
1262    /// [`App::open`]. Keep the same revision only while the configuration is compatible.
1263    /// App never derives compatibility from the opaque lower Agent identity. A
1264    /// successful open takes responsibility for consuming Agent shutdown.
1265    ///
1266    /// # Example
1267    ///
1268    /// This example requires an already-running OpenCode service at the shown endpoint.
1269    ///
1270    /// ```no_run
1271    /// use std::{num::NonZeroUsize, time::Duration};
1272    /// use agents_kit::{Agent, AgentBuilder, AgentStorage};
1273    /// use agents_kit::config::{AgentConfig, OpenCodeConfig};
1274    /// use app_kit::{AgentSpec, App, AppConfig, Shutdown};
1275    /// use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1276    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1277    /// # runtime.block_on(async {
1278    /// let agent_store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1279    /// let backend = OpenCodeConfig::external("http://127.0.0.1:4096", Duration::from_secs(10),
1280    ///     Duration::from_secs(60), Duration::from_secs(10))?;
1281    /// let agent_config = AgentConfig::new(backend.into(), Duration::from_secs(10),
1282    ///     NonZeroUsize::new(1).unwrap())?;
1283    /// let agent: Agent = AgentBuilder::create(agent_config, AgentStorage::new(agent_store)).build().await?;
1284    /// let spec = AgentSpec::new("reviewer".into(), "r1".into(), agent);
1285    /// let app_store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1286    /// let mut app = App::open(AppConfig::new(app_store, vec![spec], Vec::new())).await?;
1287    /// // App now owns consuming shutdown of the transferred Agent.
1288    /// app.shutdown(Shutdown::Drain { timeout: Duration::from_secs(30) }).await?;
1289    /// # Ok::<(), Box<dyn std::error::Error>>(())
1290    /// # })?;
1291    /// # Ok::<(), Box<dyn std::error::Error>>(())
1292    /// ```
1293    #[must_use]
1294    pub fn new(key: String, revision: String, agent: A) -> Self {
1295        Self {
1296            key,
1297            revision,
1298            agent: Arc::new(agent),
1299        }
1300    }
1301}
1302
1303impl<
1304    CS: StateStoreBackend,
1305    A: AgentBackend,
1306    X: ActionBackend,
1307    C: ConditionBackend,
1308    T: StateTyping,
1309    H: LifecycleHooks,
1310    E: ChildExecutor,
1311> Drop for App<CS, A, X, C, T, H, E>
1312{
1313    fn drop(&mut self) {
1314        self.runtime.request_drop();
1315    }
1316}
1317
1318impl<
1319    CS: StateStoreBackend,
1320    A: AgentBackend,
1321    X: ActionBackend,
1322    C: ConditionBackend,
1323    T: StateTyping,
1324    H: LifecycleHooks,
1325    E: ChildExecutor,
1326> Workspace<CS, A, X, C, T, H, E>
1327{
1328    /// Returns this workspace's stable identity.
1329    ///
1330    /// # Example
1331    ///
1332    /// ```
1333    /// # use agents_kit::Agent;
1334    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1335    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1336    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1337    /// # use storage_kit::StateStore;
1338    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1339    /// #
1340    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1341    /// # runtime.block_on(async {
1342    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1343    /// # let definition = WorkflowDefinition::parse(r#"{
1344    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1345    /// #     "state":{"count":0}, "required":["count"],
1346    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1347    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1348    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1349    /// #     ]}}], "routes":{}
1350    /// # }"#)?;
1351    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1352    /// # let mut app = App::open(config).await?;
1353    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1354    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1355    /// # }).await?;
1356    /// let id = workspace.id().clone();
1357    /// assert_eq!(workspace.snapshot().await?.id, id);
1358    /// #
1359    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1360    /// # Ok::<(), Box<dyn std::error::Error>>(())
1361    /// # })?;
1362    /// # Ok::<(), Box<dyn std::error::Error>>(())
1363    /// ```
1364    pub fn id(&self) -> &WorkspaceId {
1365        self.runtime.id()
1366    }
1367
1368    /// Loads the current workspace snapshot.
1369    ///
1370    /// # Errors
1371    ///
1372    /// Returns `Closed` after revocation or sealing, `NotFound` for absent coordination,
1373    /// `Conflict` during unresolved lifecycle progress, or `Integration` for unreadable
1374    /// committed evidence. A history-only workspace has no configuration snapshot.
1375    ///
1376    /// # Example
1377    ///
1378    /// ```
1379    /// # use agents_kit::Agent;
1380    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1381    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1382    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1383    /// # use storage_kit::StateStore;
1384    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1385    /// #
1386    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1387    /// # runtime.block_on(async {
1388    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1389    /// # let definition = WorkflowDefinition::parse(r#"{
1390    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1391    /// #     "state":{"count":0}, "required":["count"],
1392    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1393    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1394    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1395    /// #     ]}}], "routes":{}
1396    /// # }"#)?;
1397    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1398    /// # let mut app = App::open(config).await?;
1399    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1400    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1401    /// # }).await?;
1402    /// let snapshot = workspace.snapshot().await?;
1403    /// assert_eq!(snapshot.revision, 1);
1404    /// assert_eq!(snapshot.config.name.as_deref(), Some("local counter"));
1405    /// #
1406    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1407    /// # Ok::<(), Box<dyn std::error::Error>>(())
1408    /// # })?;
1409    /// # Ok::<(), Box<dyn std::error::Error>>(())
1410    /// ```
1411    pub async fn snapshot(&self) -> Result<WorkspaceSnapshot, AppError> {
1412        self.runtime.snapshot().await
1413    }
1414
1415    /// Atomically replaces the complete workspace configuration.
1416    ///
1417    /// This is full replacement, including the name and root. Preflight checks all
1418    /// lower workspaces before fan-out. A partial lower failure retains the old App
1419    /// revision and durable reverse compensation; preflight never reserves a revision.
1420    ///
1421    /// # Errors
1422    ///
1423    /// Returns `Conflict` for a stale revision, a nonterminal run pinned to the current
1424    /// revision, or partial lower fan-out (even if compensation succeeds). Invalid
1425    /// configuration, capacity, revocation, storage/lower failures and bounded waits
1426    /// report `InvalidInput`, `Busy`, `Closed`, `Integration`, or `Timeout`.
1427    ///
1428    /// # Example
1429    ///
1430    /// ```
1431    /// # use agents_kit::Agent;
1432    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1433    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1434    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1435    /// # use storage_kit::StateStore;
1436    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1437    /// #
1438    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1439    /// # runtime.block_on(async {
1440    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1441    /// # let definition = WorkflowDefinition::parse(r#"{
1442    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1443    /// #     "state":{"count":0}, "required":["count"],
1444    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1445    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1446    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1447    /// #     ]}}], "routes":{}
1448    /// # }"#)?;
1449    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1450    /// # let mut app = App::open(config).await?;
1451    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1452    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1453    /// # }).await?;
1454    /// let before = workspace.snapshot().await?;
1455    /// let after = workspace.replace(before.revision, WorkspaceConfig {
1456    ///     name: Some("reviewed".into()), root: before.config.root,
1457    /// }).await?;
1458    /// assert_eq!(after.revision, before.revision + 1);
1459    /// assert_eq!(after.config.name.as_deref(), Some("reviewed"));
1460    /// #
1461    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1462    /// # Ok::<(), Box<dyn std::error::Error>>(())
1463    /// # })?;
1464    /// # Ok::<(), Box<dyn std::error::Error>>(())
1465    /// ```
1466    pub async fn replace(
1467        &self,
1468        expected_revision: u64,
1469        config: WorkspaceConfig,
1470    ) -> Result<WorkspaceSnapshot, AppError> {
1471        self.runtime.replace(expected_revision, config).await
1472    }
1473
1474    /// Opens a configured workflow in this workspace.
1475    ///
1476    /// Selects the current configured definition for new work. A deleted workspace
1477    /// may retain a history-only workflow when exact terminal evidence still exists.
1478    ///
1479    /// # Errors
1480    ///
1481    /// Returns `NotFound` for an unavailable definition or retained scope, `Closed`
1482    /// for revoked admission, or `Integration` for inconsistent durable evidence.
1483    ///
1484    /// # Example
1485    ///
1486    /// ```
1487    /// # use agents_kit::Agent;
1488    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1489    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1490    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1491    /// # use storage_kit::StateStore;
1492    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1493    /// #
1494    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1495    /// # runtime.block_on(async {
1496    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1497    /// # let definition = WorkflowDefinition::parse(r#"{
1498    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1499    /// #     "state":{"count":0}, "required":["count"],
1500    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1501    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1502    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1503    /// #     ]}}], "routes":{}
1504    /// # }"#)?;
1505    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1506    /// # let mut app = App::open(config).await?;
1507    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1508    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1509    /// # }).await?;
1510    /// let workflow = workspace.workflow("counter".parse()?).await?;
1511    /// assert_eq!(workflow.id().to_string(), "counter");
1512    /// #
1513    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1514    /// # Ok::<(), Box<dyn std::error::Error>>(())
1515    /// # })?;
1516    /// # Ok::<(), Box<dyn std::error::Error>>(())
1517    /// ```
1518    pub async fn workflow(
1519        &self,
1520        id: WorkflowId,
1521    ) -> Result<Workflow<CS, A, X, C, T, H, E>, AppError> {
1522        Ok(Workflow::from_runtime(self.runtime.workflow(id).await?))
1523    }
1524
1525    /// Deletes this workspace after proving its revision fence.
1526    ///
1527    /// Seals new admission, requests cancellation, and deletes lower workspaces only
1528    /// after every run is terminal with no unresolved effect. Progress is durable and
1529    /// resumable; terminal run history is retained until explicit run deletion.
1530    ///
1531    /// # Errors
1532    ///
1533    /// Returns `Conflict` for a stale revision or incompatible deletion intent, `Busy`
1534    /// while unresolved work prevents deletion, and `Closed` for revoked ownership.
1535    /// Invalid fences, storage/lower failures, or expired observation return
1536    /// `InvalidInput`, `Integration`, or `Timeout`.
1537    ///
1538    /// # Example
1539    ///
1540    /// ```
1541    /// # use agents_kit::Agent;
1542    /// # use app_kit::{App, AppConfig, Shutdown, WorkspaceConfig};
1543    /// # use app_kit::workflow::{CheckpointSelector, ExecutionEvent, ExecutionOptions, RunSelector,
1544    /// #     RunStatus, WorkflowDefinition, WorkflowState};
1545    /// # use storage_kit::StateStore;
1546    /// # use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
1547    /// #
1548    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1549    /// # runtime.block_on(async {
1550    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1551    /// # let definition = WorkflowDefinition::parse(r#"{
1552    /// #     "schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
1553    /// #     "state":{"count":0}, "required":["count"],
1554    /// #     "reducers":{"count":"reject_concurrent"}, "entry":"increment",
1555    /// #     "nodes":[{"id":"increment","step":{"kind":"state","mutations":[
1556    /// #         {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
1557    /// #     ]}}], "routes":{}
1558    /// # }"#)?;
1559    /// # let config = AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition]);
1560    /// # let mut app = App::open(config).await?;
1561    /// # let mut workspace = app.create_workspace(WorkspaceConfig {
1562    /// #     name: Some("local counter".into()), root: std::env::current_dir()?,
1563    /// # }).await?;
1564    /// let revision = workspace.snapshot().await?.revision;
1565    /// workspace.delete(revision).await?;
1566    /// assert!(app.list_workspaces(None, std::num::NonZeroUsize::new(10).unwrap()).await?.is_empty());
1567    /// #
1568    /// # app.shutdown(Shutdown::CancelAndDrain { timeout: std::time::Duration::from_secs(5) }).await?;
1569    /// # Ok::<(), Box<dyn std::error::Error>>(())
1570    /// # })?;
1571    /// # Ok::<(), Box<dyn std::error::Error>>(())
1572    /// ```
1573    pub async fn delete(&mut self, expected_revision: u64) -> Result<(), AppError> {
1574        self.runtime.delete(expected_revision).await
1575    }
1576}
1577
1578impl WorkspaceId {
1579    pub(crate) fn generate() -> Self {
1580        Self(format!("appws_{}", Uuid::new_v4().hyphenated()))
1581    }
1582
1583    pub(crate) fn as_str(&self) -> &str {
1584        &self.0
1585    }
1586}
1587
1588impl IdentitySyntax {
1589    fn parse(&self, value: &str) -> Result<String, AppError> {
1590        match self {
1591            Self::Named(name) => {
1592                let valid = !value.is_empty()
1593                    && value.len() <= 128
1594                    && value.bytes().enumerate().all(|(index, byte)| {
1595                        byte.is_ascii_lowercase()
1596                            || byte.is_ascii_digit()
1597                            || (index > 0 && matches!(byte, b'_' | b'-'))
1598                    });
1599                if valid {
1600                    Ok(value.to_owned())
1601                } else {
1602                    Err(AppError::InvalidInput(format!(
1603                        "{name} must match [a-z0-9][a-z0-9_-]{{0,127}}"
1604                    )))
1605                }
1606            }
1607            Self::Uuid { name, prefix } => {
1608                let Some(encoded) = value.strip_prefix(prefix) else {
1609                    return Err(AppError::InvalidInput(format!(
1610                        "{name} has an invalid prefix"
1611                    )));
1612                };
1613                let parsed = Uuid::parse_str(encoded)
1614                    .map_err(|_| AppError::InvalidInput(format!("{name} is not a UUIDv4")))?;
1615                if parsed.get_version() != Some(Version::Random)
1616                    || parsed.get_variant() != Variant::RFC4122
1617                    || parsed.hyphenated().to_string() != encoded
1618                {
1619                    return Err(AppError::InvalidInput(format!(
1620                        "{name} must contain a canonical lowercase UUIDv4"
1621                    )));
1622                }
1623                Ok(value.to_owned())
1624            }
1625        }
1626    }
1627}
1628
1629impl fmt::Display for WorkspaceId {
1630    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1631        formatter.write_str(&self.0)
1632    }
1633}
1634
1635impl FromStr for WorkspaceId {
1636    type Err = AppError;
1637
1638    fn from_str(value: &str) -> Result<Self, Self::Err> {
1639        IdentitySyntax::Uuid {
1640            name: "WorkspaceId",
1641            prefix: "appws_",
1642        }
1643        .parse(value)
1644        .map(Self)
1645    }
1646}
1647
1648impl Serialize for WorkspaceId {
1649    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1650    where
1651        S: Serializer,
1652    {
1653        serializer.serialize_str(&self.0)
1654    }
1655}
1656
1657impl<'de> Deserialize<'de> for WorkspaceId {
1658    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1659    where
1660        D: Deserializer<'de>,
1661    {
1662        String::deserialize(deserializer)?
1663            .parse()
1664            .map_err(serde::de::Error::custom)
1665    }
1666}
1667
1668impl fmt::Display for AppError {
1669    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1670        match self {
1671            Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
1672            Self::NotFound(message) => write!(formatter, "not found: {message}"),
1673            Self::Conflict(message) => write!(formatter, "conflict: {message}"),
1674            Self::Busy(message) => write!(formatter, "busy: {message}"),
1675            Self::Closed(message) => write!(formatter, "closed: {message}"),
1676            Self::Timeout(message) => write!(formatter, "timeout: {message}"),
1677            Self::Integration(message) => write!(formatter, "integration: {message}"),
1678        }
1679    }
1680}
1681
1682impl std::error::Error for AppError {}