tandem-server 0.4.16

HTTP server for Tandem engine APIs
Documentation
pub(crate) use super::*;
use std::collections::HashMap;
use std::sync::Arc;
use tandem_core::{
    AgentRegistry, CancellationRegistry, ConfigStore, EngineLoop, EventBus, PermissionManager,
    PluginRegistry, Storage,
};
use tandem_providers::ProviderRegistry;
use tandem_runtime::{LspManager, McpRegistry, PtyManager, WorkspaceIndex};
use tandem_tools::ToolRegistry;

pub(crate) fn test_automation_node(
    node_id: &str,
    depends_on: Vec<&str>,
    phase_id: &str,
    priority: i64,
) -> AutomationFlowNode {
    AutomationFlowNode {
        node_id: node_id.to_string(),
        agent_id: "agent-a".to_string(),
        objective: format!("Run {node_id}"),
        depends_on: depends_on.into_iter().map(str::to_string).collect(),
        input_refs: Vec::new(),
        output_contract: None,
        retry_policy: None,
        timeout_ms: None,
        stage_kind: Some(AutomationNodeStageKind::Workstream),
        gate: None,
        metadata: Some(json!({
            "builder": {
                "phase_id": phase_id,
                "priority": priority
            }
        })),
    }
}

pub(crate) fn test_phase_automation(
    phases: Value,
    nodes: Vec<AutomationFlowNode>,
) -> AutomationV2Spec {
    AutomationV2Spec {
        automation_id: "auto-phase-test".to_string(),
        name: "Phase Test".to_string(),
        description: None,
        status: AutomationV2Status::Active,
        schedule: AutomationV2Schedule {
            schedule_type: AutomationV2ScheduleType::Manual,
            cron_expression: None,
            interval_seconds: None,
            timezone: "UTC".to_string(),
            misfire_policy: RoutineMisfirePolicy::RunOnce,
        },
        agents: vec![AutomationAgentProfile {
            agent_id: "agent-a".to_string(),
            template_id: Some("template-a".to_string()),
            display_name: "Agent A".to_string(),
            avatar_url: None,
            model_policy: None,
            skills: Vec::new(),
            tool_policy: AutomationAgentToolPolicy {
                allowlist: Vec::new(),
                denylist: Vec::new(),
            },
            mcp_policy: AutomationAgentMcpPolicy {
                allowed_servers: Vec::new(),
                allowed_tools: None,
            },
            approval_policy: None,
        }],
        flow: AutomationFlowSpec { nodes },
        execution: AutomationExecutionPolicy {
            max_parallel_agents: Some(2),
            max_total_runtime_ms: None,
            max_total_tool_calls: None,
            max_total_tokens: None,
            max_total_cost_usd: None,
        },
        output_targets: Vec::new(),
        created_at_ms: 1,
        updated_at_ms: 1,
        creator_id: "test".to_string(),
        workspace_root: Some(".".to_string()),
        metadata: Some(json!({
            "mission": {
                "phases": phases
            }
        })),
        next_fire_at_ms: None,
        last_fired_at_ms: None,
    }
}

pub(crate) fn test_phase_run(
    pending_nodes: Vec<&str>,
    completed_nodes: Vec<&str>,
) -> AutomationV2RunRecord {
    AutomationV2RunRecord {
        run_id: "run-phase-test".to_string(),
        automation_id: "auto-phase-test".to_string(),
        trigger_type: "manual".to_string(),
        status: AutomationRunStatus::Queued,
        created_at_ms: 1,
        updated_at_ms: 1,
        started_at_ms: None,
        finished_at_ms: None,
        active_session_ids: Vec::new(),
        latest_session_id: None,
        active_instance_ids: Vec::new(),
        checkpoint: AutomationRunCheckpoint {
            completed_nodes: completed_nodes.into_iter().map(str::to_string).collect(),
            pending_nodes: pending_nodes.into_iter().map(str::to_string).collect(),
            node_outputs: std::collections::HashMap::new(),
            node_attempts: std::collections::HashMap::new(),
            blocked_nodes: Vec::new(),
            awaiting_gate: None,
            gate_history: Vec::new(),
            lifecycle_history: Vec::new(),
            last_failure: None,
        },
        automation_snapshot: None,
        pause_reason: None,
        resume_reason: None,
        detail: None,
        stop_kind: None,
        stop_reason: None,
        prompt_tokens: 0,
        completion_tokens: 0,
        total_tokens: 0,
        estimated_cost_usd: 0.0,
    }
}

pub(crate) fn test_state_with_path(path: PathBuf) -> AppState {
    let mut state = AppState::new_starting("test-attempt".to_string(), true);
    state.shared_resources_path = path;
    state.routines_path = tmp_routines_file("shared-state");
    state.routine_history_path = tmp_routines_file("routine-history");
    state.routine_runs_path = tmp_routines_file("routine-runs");
    state.external_actions_path = tmp_routines_file("external-actions");
    state
}

pub(crate) async fn ready_test_state() -> AppState {
    let root = std::env::temp_dir().join(format!("tandem-state-test-{}", uuid::Uuid::new_v4()));
    let global = root.join("global-config.json");
    let tandem_home = root.join("tandem-home");
    let mcp_state = root.join("mcp.json");
    std::env::set_var("TANDEM_GLOBAL_CONFIG", &global);
    std::env::set_var("TANDEM_HOME", &tandem_home);
    if let Some(parent) = mcp_state.parent() {
        std::fs::create_dir_all(parent).expect("mcp state dir");
    }
    std::fs::write(&mcp_state, "{}").expect("write mcp state");

    let storage = Arc::new(Storage::new(root.join("storage")).await.expect("storage"));
    let config = ConfigStore::new(root.join("config.json"), None)
        .await
        .expect("config");
    let event_bus = EventBus::new();
    let app_config = config.get().await;
    let browser = crate::BrowserSubsystem::new(app_config.browser.clone());
    let _ = browser.refresh_status().await;
    let providers = ProviderRegistry::new(app_config.into());
    let plugins = PluginRegistry::new(".").await.expect("plugins");
    let agents = AgentRegistry::new(".").await.expect("agents");
    let tools = ToolRegistry::new();
    let permissions = PermissionManager::new(event_bus.clone());
    let mcp = McpRegistry::new_with_state_file(mcp_state);
    let pty = PtyManager::new();
    let lsp = LspManager::new(".");
    let auth = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
    let logs = Arc::new(tokio::sync::RwLock::new(Vec::new()));
    let workspace_index = WorkspaceIndex::new(".").await;
    let cancellations = CancellationRegistry::new();
    let host_runtime_context = crate::detect_host_runtime_context();
    let engine_loop = EngineLoop::new(
        storage.clone(),
        event_bus.clone(),
        providers.clone(),
        plugins.clone(),
        agents.clone(),
        permissions.clone(),
        tools.clone(),
        cancellations.clone(),
        host_runtime_context.clone(),
    );
    let mut state = AppState::new_starting(uuid::Uuid::new_v4().to_string(), false);
    state.shared_resources_path = root.join("shared_resources.json");
    state
        .mark_ready(crate::RuntimeState {
            storage,
            config,
            event_bus,
            providers,
            plugins,
            agents,
            tools,
            permissions,
            mcp,
            pty,
            lsp,
            auth,
            logs,
            workspace_index,
            cancellations,
            engine_loop,
            host_runtime_context,
            browser,
        })
        .await
        .expect("runtime ready");
    state
}

pub(crate) fn tmp_resource_file(name: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "tandem-server-{name}-{}.json",
        uuid::Uuid::new_v4()
    ))
}

pub(crate) fn tmp_routines_file(name: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "tandem-server-routines-{name}-{}.json",
        uuid::Uuid::new_v4()
    ))
}

mod automations;
mod routines;
mod shared_resources;
mod status_index;