use std::{
collections::{BTreeMap, HashMap},
path::PathBuf,
};
use serde::{Deserialize, Serialize};
pub const DEFAULT_PROMPT_TEMPLATE: &str = "You are working on an issue from Linear.";
pub const DEFAULT_LINEAR_ENDPOINT: &str = "https://api.linear.app/graphql";
pub const DEFAULT_POLL_INTERVAL_MS: u64 = 30_000;
pub const DEFAULT_WORKSPACE_ROOT: &str = "/symphony_workspaces";
pub const DEFAULT_HOOK_TIMEOUT_MS: u64 = 60_000;
pub const DEFAULT_MAX_CONCURRENT_AGENTS: u64 = 10;
pub const DEFAULT_MAX_TURNS: u64 = 20;
pub const DEFAULT_MAX_RETRY_BACKOFF_MS: u64 = 300_000;
pub const DEFAULT_STALL_TIMEOUT_MS: u64 = 300_000;
pub const DEFAULT_OPENHANDS_BASE_URL: &str = "http://127.0.0.1:8000";
pub const DEFAULT_OPENHANDS_STARTUP_TIMEOUT_MS: u64 = 180_000;
pub const DEFAULT_OPENHANDS_READINESS_PROBE_PATH: &str = "/openapi.json";
pub const DEFAULT_OPENHANDS_PERSISTENCE_DIR: &str = ".opensymphony/openhands";
pub const DEFAULT_OPENHANDS_MAX_ITERATIONS: u64 = 500;
pub const DEFAULT_OPENHANDS_CONFIRMATION_POLICY_KIND: &str = "NeverConfirm";
pub const DEFAULT_OPENHANDS_AGENT_KIND: &str = "Agent";
pub const DEFAULT_OPENHANDS_AGENT_TOOLS: &[&str] = &["terminal", "file_editor"];
pub const DEFAULT_OPENHANDS_READY_TIMEOUT_MS: u64 = 30_000;
pub const DEFAULT_OPENHANDS_RECONNECT_INITIAL_MS: u64 = 1_000;
pub const DEFAULT_OPENHANDS_RECONNECT_MAX_MS: u64 = 30_000;
pub const DEFAULT_OPENHANDS_AUTH_MODE: &str = "auto";
pub const DEFAULT_OPENHANDS_QUERY_PARAM_NAME: &str = "session_api_key";
pub const DEFAULT_OPENHANDS_LLM_MODEL: &str = "openai/gpt-5.4";
pub const DEFAULT_OPENHANDS_CONDENSER_MAX_SIZE: u64 = 240;
pub const DEFAULT_OPENHANDS_CONDENSER_KEEP_FIRST: u64 = 2;
#[derive(Debug, Clone, PartialEq)]
pub struct WorkflowDefinition {
pub front_matter: WorkflowFrontMatter,
pub prompt_template: String,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct WorkflowFrontMatter {
#[serde(default)]
pub tracker: TrackerFrontMatter,
#[serde(default)]
pub polling: PollingFrontMatter,
#[serde(default)]
pub workspace: WorkspaceFrontMatter,
#[serde(default)]
pub hooks: HooksFrontMatter,
#[serde(default)]
pub agent: AgentFrontMatter,
#[serde(default)]
pub openhands: OpenHandsFrontMatter,
#[serde(default)]
pub codex: Option<BTreeMap<String, serde_yaml::Value>>,
#[serde(default)]
pub logging: Option<BTreeMap<String, serde_yaml::Value>>,
#[serde(flatten)]
pub extensions: BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct TrackerFrontMatter {
pub kind: Option<String>,
pub endpoint: Option<String>,
pub api_key: Option<String>,
pub project_slug: Option<String>,
pub active_states: Option<Vec<String>>,
pub terminal_states: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct PollingFrontMatter {
pub interval_ms: Option<IntegerLike>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceFrontMatter {
pub root: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct HooksFrontMatter {
pub after_create: Option<String>,
pub before_run: Option<String>,
pub after_run: Option<String>,
pub before_remove: Option<String>,
pub timeout_ms: Option<IntegerLike>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct AgentFrontMatter {
pub max_concurrent_agents: Option<IntegerLike>,
pub max_turns: Option<IntegerLike>,
pub max_retry_backoff_ms: Option<IntegerLike>,
pub stall_timeout_ms: Option<IntegerLike>,
pub max_concurrent_agents_by_state: Option<BTreeMap<String, IntegerLike>>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsFrontMatter {
#[serde(default)]
pub transport: OpenHandsTransportFrontMatter,
#[serde(default)]
pub local_server: OpenHandsLocalServerFrontMatter,
#[serde(default)]
pub conversation: OpenHandsConversationFrontMatter,
#[serde(default)]
pub websocket: OpenHandsWebSocketFrontMatter,
#[serde(default, rename = "mcp")]
pub legacy_linear_bridge: Option<serde_yaml::Value>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsTransportFrontMatter {
pub base_url: Option<String>,
pub session_api_key_env: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsLocalServerFrontMatter {
pub enabled: Option<bool>,
pub command: Option<Vec<String>>,
pub startup_timeout_ms: Option<IntegerLike>,
pub readiness_probe_path: Option<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsConversationFrontMatter {
pub reuse_policy: Option<String>,
pub persistence_dir_relative: Option<String>,
pub max_iterations: Option<IntegerLike>,
pub stuck_detection: Option<bool>,
pub confirmation_policy: Option<OpenHandsConfirmationPolicyFrontMatter>,
pub agent: Option<OpenHandsConversationAgentFrontMatter>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct OpenHandsConfirmationPolicyFrontMatter {
pub kind: Option<String>,
#[serde(flatten)]
pub options: BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct OpenHandsConfirmationPolicy {
pub kind: String,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct OpenHandsConversationAgentFrontMatter {
pub kind: Option<String>,
pub llm: Option<OpenHandsLlmFrontMatter>,
pub condenser: Option<OpenHandsConversationCondenserFrontMatter>,
pub tools: Option<Vec<OpenHandsConversationToolFrontMatter>>,
pub include_default_tools: Option<Vec<String>>,
pub log_completions: Option<bool>,
#[serde(flatten)]
pub options: BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsConversationToolFrontMatter {
pub name: String,
#[serde(default)]
pub params: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsConversationCondenserFrontMatter {
pub enabled: Option<bool>,
pub max_size: Option<IntegerLike>,
pub keep_first: Option<IntegerLike>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct OpenHandsLlmFrontMatter {
pub model: Option<String>,
pub api_key_env: Option<String>,
pub base_url_env: Option<String>,
#[serde(flatten)]
pub options: BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OpenHandsWebSocketFrontMatter {
pub enabled: Option<bool>,
pub ready_timeout_ms: Option<IntegerLike>,
pub reconnect_initial_ms: Option<IntegerLike>,
pub reconnect_max_ms: Option<IntegerLike>,
pub auth_mode: Option<String>,
pub query_param_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum IntegerLike {
Integer(i64),
String(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedWorkflow {
pub config: WorkflowConfig,
pub extensions: WorkflowExtensions,
pub prompt_template: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkflowConfig {
pub tracker: TrackerConfig,
pub polling: PollingConfig,
pub workspace: WorkspaceConfig,
pub hooks: HooksConfig,
pub agent: AgentConfig,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WorkflowExtensions {
pub openhands: OpenHandsConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackerKind {
Linear,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrackerConfig {
pub kind: TrackerKind,
pub endpoint: String,
pub api_key: String,
pub project_slug: String,
pub active_states: Vec<String>,
pub terminal_states: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PollingConfig {
pub interval_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceConfig {
pub root: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HooksConfig {
pub after_create: Option<String>,
pub before_run: Option<String>,
pub after_run: Option<String>,
pub before_remove: Option<String>,
pub timeout_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentConfig {
pub max_concurrent_agents: u64,
pub max_turns: u64,
pub max_retry_backoff_ms: u64,
pub stall_timeout_ms: Option<u64>,
pub max_concurrent_agents_by_state: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OpenHandsConfig {
pub transport: OpenHandsTransportConfig,
pub local_server: OpenHandsLocalServerConfig,
pub conversation: OpenHandsConversationConfig,
pub websocket: OpenHandsWebSocketConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenHandsTransportConfig {
pub base_url: String,
pub session_api_key_env: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenHandsLocalServerConfig {
pub enabled: bool,
pub command: Option<Vec<String>>,
pub startup_timeout_ms: u64,
pub readiness_probe_path: String,
pub env: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OpenHandsConversationConfig {
pub reuse_policy: String,
pub persistence_dir_relative: PathBuf,
pub max_iterations: u64,
pub stuck_detection: bool,
pub confirmation_policy: OpenHandsConfirmationPolicy,
pub agent: OpenHandsConversationAgentConfig,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OpenHandsConversationAgentConfig {
pub kind: String,
pub llm: Option<OpenHandsLlmConfig>,
pub condenser: Option<OpenHandsConversationCondenserConfig>,
pub tools: Option<Vec<OpenHandsConversationToolConfig>>,
pub include_default_tools: Option<Vec<String>>,
pub log_completions: bool,
pub options: BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenHandsConversationToolConfig {
pub name: String,
pub params: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenHandsConversationCondenserConfig {
pub max_size: u64,
pub keep_first: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OpenHandsLlmConfig {
pub model: Option<String>,
pub api_key_env: Option<String>,
pub base_url_env: Option<String>,
pub options: BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenHandsWebSocketConfig {
pub enabled: bool,
pub ready_timeout_ms: u64,
pub reconnect_initial_ms: u64,
pub reconnect_max_ms: u64,
pub auth_mode: String,
pub query_param_name: String,
}
pub trait Environment {
fn get(&self, name: &str) -> Option<String>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ProcessEnvironment;
impl Environment for ProcessEnvironment {
fn get(&self, name: &str) -> Option<String> {
std::env::var_os(name).map(|value| value.to_string_lossy().into_owned())
}
}
impl Environment for BTreeMap<String, String> {
fn get(&self, name: &str) -> Option<String> {
self.get(name).cloned()
}
}
impl Environment for HashMap<String, String> {
fn get(&self, name: &str) -> Option<String> {
self.get(name).cloned()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PromptContext<'a, T>
where
T: Serialize,
{
pub issue: &'a T,
pub attempt: Option<u32>,
}