roder-core 0.3.2

Agentic software development tools and SDKs for Roder.
Documentation
use async_trait::async_trait;
use roder_api::catalog::{PROVIDER_MOCK, REASONING_NONE, REASONING_XHIGH};
use roder_api::dynamic_workflows::WorkflowRunLimits;
use roder_api::inference::{InstructionBundle, ReasoningConfig, RuntimeProfile};
use roder_core::{
    DynamicWorkflowEffortProfile, RuntimeConfig, RuntimeDynamicWorkflowConfig, WorkflowPlanner,
    WorkflowPlannerRequest, WorkflowScriptDraftSource, WorkflowTriggerDecision,
    WorkflowTriggerKind, WorkflowTriggerSuppression, classify_workflow_trigger,
};
use roder_dynamic_workflows::{
    WorkflowApprovalScope, WorkflowConsentKey, WorkflowConsentStore, workflow_script_hash,
};
use time::OffsetDateTime;

const FIXTURE_WORKFLOW: &str = r#"
workflow.define({
  name: "Crate Audit",
  description: "Audit crates and review findings.",
  hostApiVersion: 1,
  phases: ["scout", "review"],
  limits: { maxAgentsPerRun: 12, maxConcurrentAgents: 4 }
}, async (ctx) => {
  const findings = await ctx.agents.map("scout", ["api", "core"], (crateName) => ({
    lane: "scout",
    prompt: `Audit ${crateName}`
  }));
  ctx.checkpoint.save("findings", findings);
  return ctx.report.markdown(findings.map((finding) => finding.output).join("\n"));
});
"#;

#[derive(Clone)]
struct StaticDraftSource {
    source: String,
}

#[async_trait]
impl WorkflowScriptDraftSource for StaticDraftSource {
    async fn draft_workflow_script(
        &self,
        request: &WorkflowPlannerRequest,
    ) -> anyhow::Result<String> {
        assert!(request.prompt.contains("audit"));
        assert_eq!(request.provider, PROVIDER_MOCK);
        Ok(self.source.clone())
    }
}

#[tokio::test]
async fn prompt_triggered_planning_uses_fake_model_and_validates_script() {
    let config = RuntimeDynamicWorkflowConfig::default();
    let trigger = classify_workflow_trigger(
        "Use a workflow to audit the crates and review findings",
        &config,
    );
    assert_eq!(
        trigger,
        WorkflowTriggerDecision::Plan(WorkflowTriggerKind::TriggerWord)
    );

    let planner = WorkflowPlanner::new(StaticDraftSource {
        source: FIXTURE_WORKFLOW.to_string(),
    });
    let draft = planner
        .plan(WorkflowPlannerRequest {
            prompt: "Use a workflow to audit the crates".to_string(),
            workspace: Some("/workspace".to_string()),
            source_path: None,
            provider: PROVIDER_MOCK.to_string(),
            model: "mock".to_string(),
            reasoning: ReasoningConfig::default(),
            runtime_profile: RuntimeProfile::Interactive,
            effort_profile: DynamicWorkflowEffortProfile::Standard,
            trigger: WorkflowTriggerKind::TriggerWord,
            instructions: InstructionBundle::default(),
            limits: WorkflowRunLimits::default(),
        })
        .await
        .unwrap();

    assert_eq!(draft.script.name, "Crate Audit");
    assert_eq!(draft.script.hash, workflow_script_hash(FIXTURE_WORKFLOW));
    assert_eq!(
        draft.phase_names,
        vec!["scout".to_string(), "review".to_string()]
    );
    assert_eq!(
        draft.capability_scope,
        vec![
            "childAgents".to_string(),
            "checkpoints".to_string(),
            "reports".to_string()
        ]
    );
    assert_eq!(draft.cost_estimate.max_child_agents, 12);
}

#[test]
fn ignored_trigger_word_does_not_plan_for_chat_or_slash_commands() {
    let config = RuntimeDynamicWorkflowConfig::default();

    assert_eq!(
        classify_workflow_trigger("What is a workflow?", &config),
        WorkflowTriggerDecision::Ignore(WorkflowTriggerSuppression::PureChatQuestion)
    );
    assert_eq!(
        classify_workflow_trigger("/workflows", &config),
        WorkflowTriggerDecision::Ignore(WorkflowTriggerSuppression::SlashCommand)
    );
}

#[test]
fn consent_reuse_is_scoped_to_script_workspace_source_and_capabilities() {
    let now = OffsetDateTime::UNIX_EPOCH;
    let hash = workflow_script_hash(FIXTURE_WORKFLOW);
    let key = WorkflowConsentKey::new(
        hash.clone(),
        Some("/workspace"),
        Some(".agents/workflows/audit.workflow.js"),
        WorkflowApprovalScope::ScriptAndWorkspace,
    );
    let mut store = WorkflowConsentStore::default();
    store.record(
        key.clone(),
        vec!["childAgents".to_string(), "reports".to_string()],
        now,
        None,
    );

    assert!(
        store
            .reusable_consent(&key, now, &["childAgents".to_string()])
            .is_some()
    );

    let other_source = WorkflowConsentKey::new(
        hash,
        Some("/workspace"),
        Some(".agents/workflows/other.workflow.js"),
        WorkflowApprovalScope::ScriptAndWorkspace,
    );
    assert!(
        store
            .reusable_consent(&other_source, now, &["childAgents".to_string()])
            .is_none()
    );
    assert!(
        store
            .reusable_consent(&key, now, &["shell".to_string()])
            .is_none()
    );
}

#[test]
fn ultracode_degrades_on_models_without_xhigh() {
    let mock = RuntimeConfig {
        dynamic_workflows: RuntimeDynamicWorkflowConfig {
            effort_profile: DynamicWorkflowEffortProfile::Ultracode,
            ..RuntimeDynamicWorkflowConfig::default()
        },
        ..RuntimeConfig::default()
    };
    assert_eq!(
        roder_core::Runtime::effective_reasoning_for_config(&mock),
        REASONING_NONE
    );

    let supported = RuntimeConfig {
        default_model: "gpt-5.5".to_string(),
        dynamic_workflows: RuntimeDynamicWorkflowConfig {
            effort_profile: DynamicWorkflowEffortProfile::Ultracode,
            ..RuntimeDynamicWorkflowConfig::default()
        },
        ..RuntimeConfig::default()
    };
    assert_eq!(
        roder_core::Runtime::effective_reasoning_for_config(&supported),
        REASONING_XHIGH
    );
}