#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::fmt;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use agents_kit::AgentBackend;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use storage_kit::StateStoreBackend;
use uuid::{Uuid, Variant, Version};
use crate::workflow::{
ActionBackend, ChildExecutor, ConditionBackend, InlineExecutor, JsonState, LifecycleHooks,
NoActions, NoConditions, NoHooks, StateTyping, Workflow, WorkflowDefinition, WorkflowId,
};
mod app;
mod storage;
mod workspace;
pub mod workflow;
pub struct App<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend = NoActions,
C: ConditionBackend = NoConditions,
T: StateTyping = JsonState,
H: LifecycleHooks = NoHooks,
E: ChildExecutor = InlineExecutor,
> {
runtime: Arc<app::AppRuntime<CS, A, X, C, T, H, E>>,
}
pub struct AppConfig<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend = NoActions,
C: ConditionBackend = NoConditions,
T: StateTyping = JsonState,
H: LifecycleHooks = NoHooks,
E: ChildExecutor = InlineExecutor,
> {
state: CS,
agents: Vec<AgentSpec<A>>,
workflows: Vec<WorkflowDefinition>,
actions: Vec<X>,
conditions: Vec<C>,
typing: T,
hooks: H,
executor: E,
}
pub struct AgentSpec<A: AgentBackend> {
key: String,
revision: String,
agent: Arc<A>,
}
pub enum Shutdown {
RejectActive,
Drain {
timeout: Duration,
},
CancelAndDrain {
timeout: Duration,
},
}
pub struct Workspace<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend = NoActions,
C: ConditionBackend = NoConditions,
T: StateTyping = JsonState,
H: LifecycleHooks = NoHooks,
E: ChildExecutor = InlineExecutor,
> {
runtime: workspace::WorkspaceRuntime<CS, A, X, C, T, H, E>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WorkspaceId(String);
pub struct WorkspaceConfig {
pub name: Option<String>,
pub root: PathBuf,
}
#[non_exhaustive]
pub struct WorkspaceSnapshot {
pub id: WorkspaceId,
pub revision: u64,
pub config: WorkspaceConfig,
}
#[non_exhaustive]
#[derive(Debug)]
pub enum AppError {
InvalidInput(String),
NotFound(String),
Conflict(String),
Busy(String),
Closed(String),
Timeout(String),
Integration(String),
}
enum IdentitySyntax {
Named(&'static str),
Uuid {
name: &'static str,
prefix: &'static str,
},
}
impl<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend,
C: ConditionBackend,
T: StateTyping,
H: LifecycleHooks,
E: ChildExecutor,
> App<CS, A, X, C, T, H, E>
{
pub async fn open(config: AppConfig<CS, A, X, C, T, H, E>) -> Result<Self, AppError> {
Ok(Self {
runtime: app::AppTrace::span(|| tracing::info_span!(target: "app_kit", "app.open"))
.observe(std::pin::pin!(app::AppRuntime::open(config)))
.await?,
})
}
pub async fn create_workspace(
&self,
config: WorkspaceConfig,
) -> Result<Workspace<CS, A, X, C, T, H, E>, AppError> {
Ok(Workspace {
runtime: self.runtime.create_workspace(config).await?,
})
}
pub async fn workspace(
&self,
id: WorkspaceId,
) -> Result<Workspace<CS, A, X, C, T, H, E>, AppError> {
Ok(Workspace {
runtime: self.runtime.workspace(id).await?,
})
}
pub async fn list_workspaces(
&self,
after: Option<WorkspaceId>,
limit: NonZeroUsize,
) -> Result<Vec<WorkspaceSnapshot>, AppError> {
self.runtime.list_workspaces(after, limit).await
}
pub async fn shutdown(&mut self, policy: Shutdown) -> Result<(), AppError> {
self.runtime.shutdown(policy).await
}
}
impl<CS: StateStoreBackend, A: AgentBackend> AppConfig<CS, A> {
#[must_use]
pub fn new(state: CS, agents: Vec<AgentSpec<A>>, workflows: Vec<WorkflowDefinition>) -> Self {
Self {
state,
agents,
workflows,
actions: Vec::new(),
conditions: Vec::new(),
typing: JsonState,
hooks: NoHooks,
executor: InlineExecutor,
}
}
}
impl<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend,
C: ConditionBackend,
T: StateTyping,
H: LifecycleHooks,
E: ChildExecutor,
> AppConfig<CS, A, X, C, T, H, E>
{
#[must_use]
pub fn with_actions<N: ActionBackend>(
self,
actions: Vec<N>,
) -> AppConfig<CS, A, N, C, T, H, E> {
AppConfig {
state: self.state,
agents: self.agents,
workflows: self.workflows,
actions,
conditions: self.conditions,
typing: self.typing,
hooks: self.hooks,
executor: self.executor,
}
}
#[must_use]
pub fn with_conditions<N: ConditionBackend>(
self,
conditions: Vec<N>,
) -> AppConfig<CS, A, X, N, T, H, E> {
AppConfig {
state: self.state,
agents: self.agents,
workflows: self.workflows,
actions: self.actions,
conditions,
typing: self.typing,
hooks: self.hooks,
executor: self.executor,
}
}
#[must_use]
pub fn with_state_typing<N: StateTyping>(self, typing: N) -> AppConfig<CS, A, X, C, N, H, E> {
AppConfig {
state: self.state,
agents: self.agents,
workflows: self.workflows,
actions: self.actions,
conditions: self.conditions,
typing,
hooks: self.hooks,
executor: self.executor,
}
}
#[must_use]
pub fn with_hooks<N: LifecycleHooks>(self, hooks: N) -> AppConfig<CS, A, X, C, T, N, E> {
AppConfig {
state: self.state,
agents: self.agents,
workflows: self.workflows,
actions: self.actions,
conditions: self.conditions,
typing: self.typing,
hooks,
executor: self.executor,
}
}
#[must_use]
pub fn with_child_executor<N: ChildExecutor>(
self,
executor: N,
) -> AppConfig<CS, A, X, C, T, H, N> {
AppConfig {
state: self.state,
agents: self.agents,
workflows: self.workflows,
actions: self.actions,
conditions: self.conditions,
typing: self.typing,
hooks: self.hooks,
executor,
}
}
}
impl<A: AgentBackend> AgentSpec<A> {
#[must_use]
pub fn new(key: String, revision: String, agent: A) -> Self {
Self {
key,
revision,
agent: Arc::new(agent),
}
}
}
impl<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend,
C: ConditionBackend,
T: StateTyping,
H: LifecycleHooks,
E: ChildExecutor,
> Drop for App<CS, A, X, C, T, H, E>
{
fn drop(&mut self) {
self.runtime.request_drop();
}
}
impl<
CS: StateStoreBackend,
A: AgentBackend,
X: ActionBackend,
C: ConditionBackend,
T: StateTyping,
H: LifecycleHooks,
E: ChildExecutor,
> Workspace<CS, A, X, C, T, H, E>
{
pub fn id(&self) -> &WorkspaceId {
self.runtime.id()
}
pub async fn snapshot(&self) -> Result<WorkspaceSnapshot, AppError> {
self.runtime.snapshot().await
}
pub async fn replace(
&self,
expected_revision: u64,
config: WorkspaceConfig,
) -> Result<WorkspaceSnapshot, AppError> {
self.runtime.replace(expected_revision, config).await
}
pub async fn workflow(
&self,
id: WorkflowId,
) -> Result<Workflow<CS, A, X, C, T, H, E>, AppError> {
Ok(Workflow::from_runtime(self.runtime.workflow(id).await?))
}
pub async fn delete(&mut self, expected_revision: u64) -> Result<(), AppError> {
self.runtime.delete(expected_revision).await
}
}
impl WorkspaceId {
pub(crate) fn generate() -> Self {
Self(format!("appws_{}", Uuid::new_v4().hyphenated()))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl IdentitySyntax {
fn parse(&self, value: &str) -> Result<String, AppError> {
match self {
Self::Named(name) => {
let valid = !value.is_empty()
&& value.len() <= 128
&& value.bytes().enumerate().all(|(index, byte)| {
byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| (index > 0 && matches!(byte, b'_' | b'-'))
});
if valid {
Ok(value.to_owned())
} else {
Err(AppError::InvalidInput(format!(
"{name} must match [a-z0-9][a-z0-9_-]{{0,127}}"
)))
}
}
Self::Uuid { name, prefix } => {
let Some(encoded) = value.strip_prefix(prefix) else {
return Err(AppError::InvalidInput(format!(
"{name} has an invalid prefix"
)));
};
let parsed = Uuid::parse_str(encoded)
.map_err(|_| AppError::InvalidInput(format!("{name} is not a UUIDv4")))?;
if parsed.get_version() != Some(Version::Random)
|| parsed.get_variant() != Variant::RFC4122
|| parsed.hyphenated().to_string() != encoded
{
return Err(AppError::InvalidInput(format!(
"{name} must contain a canonical lowercase UUIDv4"
)));
}
Ok(value.to_owned())
}
}
}
}
impl fmt::Display for WorkspaceId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for WorkspaceId {
type Err = AppError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
IdentitySyntax::Uuid {
name: "WorkspaceId",
prefix: "appws_",
}
.parse(value)
.map(Self)
}
}
impl Serialize for WorkspaceId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for WorkspaceId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
}
impl fmt::Display for AppError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
Self::NotFound(message) => write!(formatter, "not found: {message}"),
Self::Conflict(message) => write!(formatter, "conflict: {message}"),
Self::Busy(message) => write!(formatter, "busy: {message}"),
Self::Closed(message) => write!(formatter, "closed: {message}"),
Self::Timeout(message) => write!(formatter, "timeout: {message}"),
Self::Integration(message) => write!(formatter, "integration: {message}"),
}
}
}
impl std::error::Error for AppError {}