Skip to main content

distri_types/configuration/
package.rs

1use crate::ToolDefinition;
2use crate::agent::StandardDefinition;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6
7/// Cloud-specific metadata for agents (optional, only present in cloud responses).
8/// The marketplace surface (`published`, `published_at`, `is_system`, the
9/// "agent from another workspace" cross-publish concept) was removed.
10/// Agents are workspace-scoped only.
11#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
12pub struct AgentCloudMetadata {
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub is_owner: Option<bool>,
15    /// True when the agent belongs to the current workspace.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub is_workspace: Option<bool>,
18    /// Workspace slug the agent belongs to.
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub workspace_slug: Option<String>,
21    /// When the agent row was last modified. Used as a cache epoch — e.g. the
22    /// gateway keys its compiled `CommandRouter` on `(agent_id, updated_at)`
23    /// so a workflow/command edit invalidates the router.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
29pub struct AgentConfigWithTools {
30    #[serde(flatten)]
31    #[schema(value_type = Object)]
32    pub agent: AgentConfig,
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub resolved_tools: Vec<ToolDefinition>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub markdown: Option<String>,
37    /// Cloud-specific metadata (optional, only present in cloud responses)
38    #[serde(flatten, default)]
39    pub cloud: AgentCloudMetadata,
40}
41
42/// Unified agent configuration enum
43#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
44#[serde(tag = "agent_type", rename_all = "snake_case")]
45#[allow(clippy::large_enum_variant)]
46pub enum AgentConfig {
47    /// Standard markdown-based agent
48    #[schema(value_type = Object)]
49    StandardAgent(StandardDefinition),
50    /// Workflow-based agent — executes a workflow DAG instead of an LLM loop
51    #[schema(value_type = Object)]
52    WorkflowAgent(WorkflowAgentDefinition),
53}
54
55/// Definition for a workflow-based agent.
56/// The workflow definition is stored as JSON to avoid crate dependency on distri-workflow.
57/// Deserialize to `distri_workflow::WorkflowDefinition` at execution time.
58#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59pub struct WorkflowAgentDefinition {
60    pub name: String,
61    pub description: String,
62    #[serde(default = "default_version")]
63    pub version: String,
64    /// The workflow definition as JSON.
65    pub definition: serde_json::Value,
66    /// JSON Schema for required inputs (validated before execution).
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub input_schema: Option<serde_json::Value>,
69    /// Agent-level triggers — applied to the default entry point.
70    /// Empty = `Manual` only (direct API/UI invocation). Entry-point
71    /// triggers (in `WorkflowDefinition.entry_points[*].triggers`)
72    /// take precedence per-entry-point.
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub triggers: Vec<crate::WorkflowTrigger>,
75    /// Channel chrome when this workflow agent backs a bot. Optional.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub channels: Option<crate::channel_commands::ChannelBindings>,
78    /// Workspace connections this workflow needs. Resolved at run start —
79    /// each connection's tokens are injected into `ExecutorContext.env_vars`
80    /// (usable from `api_call` steps via `{env.X}`), and the connection's
81    /// MCP tools become available to `tool_call` steps by name.
82    ///
83    /// Same shape as `StandardDefinition.connections`; the connection's
84    /// own `auth_scope` (Workspace vs User) determines the fire mode for
85    /// triggered runs.
86    #[serde(default, skip_serializing_if = "Vec::is_empty")]
87    pub connections: Vec<crate::connections::ConnectionRequirement>,
88    /// Explicit tool allowlist (mirrors `StandardDefinition.tools`).
89    /// Optional — when absent, the workflow agent gets the MCP tools
90    /// implied by its declared `connections` plus nothing else.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub tools: Option<crate::ToolsConfig>,
93}
94
95fn default_version() -> String {
96    "0.1.0".to_string()
97}
98
99impl AgentConfig {
100    /// Get the name of the agent
101    pub fn get_name(&self) -> &str {
102        match self {
103            AgentConfig::StandardAgent(def) => &def.name,
104            AgentConfig::WorkflowAgent(def) => &def.name,
105        }
106    }
107
108    pub fn get_definition(&self) -> &StandardDefinition {
109        match self {
110            AgentConfig::StandardAgent(def) => def,
111            AgentConfig::WorkflowAgent(_) => {
112                panic!("WorkflowAgent does not have a StandardDefinition")
113            }
114        }
115    }
116
117    /// Resolved agent version. Standard agents fall back to
118    /// `default_agent_version()`; workflow agents carry a required version.
119    pub fn version(&self) -> Option<String> {
120        match self {
121            AgentConfig::StandardAgent(def) => def
122                .version
123                .clone()
124                .or_else(crate::agent::default_agent_version),
125            AgentConfig::WorkflowAgent(def) => Some(def.version.clone()),
126        }
127    }
128
129    /// Get the description of the agent
130    pub fn get_description(&self) -> &str {
131        match self {
132            AgentConfig::StandardAgent(def) => &def.description,
133            AgentConfig::WorkflowAgent(def) => &def.description,
134        }
135    }
136
137    /// Get the tools configuration, if this is a standard agent.
138    pub fn get_tools_config(&self) -> Option<&crate::ToolsConfig> {
139        match self {
140            AgentConfig::StandardAgent(def) => def.tools.as_ref(),
141            AgentConfig::WorkflowAgent(_) => None,
142        }
143    }
144
145    /// Get schedule triggers for this agent (only workflow agents
146    /// can have them). Walks both the agent-level `triggers` and
147    /// each entry point's `triggers` since both layers may declare
148    /// `Schedule`.
149    pub fn get_schedule_triggers(&self) -> Vec<&crate::WorkflowTrigger> {
150        match self {
151            AgentConfig::StandardAgent(_) => vec![],
152            AgentConfig::WorkflowAgent(def) => def
153                .triggers
154                .iter()
155                .filter(|t| matches!(t, crate::WorkflowTrigger::Schedule { .. }))
156                .collect(),
157        }
158    }
159
160    /// Validate the configuration
161    pub fn validate(&self) -> anyhow::Result<()> {
162        match self {
163            AgentConfig::StandardAgent(def) => def.validate(),
164            AgentConfig::WorkflowAgent(_def) => Ok(()), // Workflow validation happens at execution
165        }
166    }
167
168    /// Build the public A2A [`AgentCard`] for this agent.
169    ///
170    /// This is the **cheap, card-only** projection of an agent: it pulls just the
171    /// discovery metadata (name, description, version, icon, skills) and combines
172    /// it with the server's static A2A settings. It deliberately does **not**
173    /// touch the system prompt / instructions, tools, model settings, or any
174    /// other execution config — callers that only need the card (the public
175    /// `.well-known/agent.json` endpoint, agent listings, CLI agent pickers)
176    /// should go through here instead of loading + matching the full definition.
177    pub fn to_card(&self, server_config: &crate::configuration::ServerConfig) -> distri_a2a::AgentCard {
178        let (name, description, version, icon_url, skills) = match self {
179            AgentConfig::StandardAgent(def) => (
180                def.name.clone(),
181                def.description.clone(),
182                def.version.clone(),
183                def.icon_url.clone(),
184                def.skills_description.clone(),
185            ),
186            AgentConfig::WorkflowAgent(def) => (
187                def.name.clone(),
188                def.description.clone(),
189                Some(def.version.clone()),
190                None,
191                Vec::new(),
192            ),
193        };
194
195        let version = version
196            .or_else(crate::agent::default_agent_version)
197            .unwrap_or_else(|| distri_a2a::A2A_VERSION.to_string());
198
199        distri_a2a::AgentCard {
200            version,
201            url: format!("{}/agents/{}", server_config.base_url, name),
202            name,
203            description,
204            icon_url,
205            documentation_url: server_config.documentation_url.clone(),
206            provider: Some(server_config.agent_provider.clone()),
207            preferred_transport: server_config.preferred_transport.clone(),
208            capabilities: server_config.capabilities.clone(),
209            default_input_modes: server_config.default_input_modes.clone(),
210            default_output_modes: server_config.default_output_modes.clone(),
211            skills,
212            security_schemes: server_config.security_schemes.clone(),
213            security: server_config.security.clone(),
214        }
215    }
216}
217
218#[cfg(test)]
219mod channel_binding_tests {
220    use super::*;
221
222    #[test]
223    fn workflow_agent_accepts_channels_field() {
224        let json = serde_json::json!({
225            "name": "z", "description": "d",
226            "definition": {"id":"w","steps":[]},
227            "channels": {"telegram": {"web_app_base": "https://a.app"}}
228        });
229        let def: WorkflowAgentDefinition = serde_json::from_value(json).unwrap();
230        assert!(def.channels.is_some());
231    }
232
233    #[test]
234    fn workflow_agent_channels_optional() {
235        let json = serde_json::json!({
236            "name": "z", "description": "d", "definition": {"id":"w","steps":[]}
237        });
238        let def: WorkflowAgentDefinition = serde_json::from_value(json).unwrap();
239        assert!(def.channels.is_none());
240    }
241}