a3s-code-core 8.5.4

A3S Code Core - Embeddable AI agent library with tool execution
Documentation
use super::*;

#[tokio::test]
async fn test_from_config() {
    let agent = Agent::from_config(test_config()).await;
    assert!(agent.is_ok());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn agent_scheduler_prioritizes_interactive_work_across_sessions() {
    let workspace = tempfile::tempdir().unwrap();
    let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
    let release = Arc::new(tokio::sync::Semaphore::new(0));
    let client = Arc::new(TaskSchedulerProbeClient {
        started: started_tx,
        release: Arc::clone(&release),
    });
    let mut config = test_config();
    config.task_scheduler = crate::task_scheduler::TaskSchedulerConfig {
        max_active: 1,
        aging_interval_ms: 60_000,
    };
    config.memory = Some(crate::memory::MemoryConfig {
        llm_extraction: false,
        ..Default::default()
    });
    let agent = Agent::from_config(config).await.unwrap();

    let make_session = |id: &str, priority| {
        SessionOptions::new()
            .with_session_id(id)
            .with_llm_client(client.clone())
            .with_planning_mode(crate::prompts::PlanningMode::Disabled)
            .with_continuation(false)
            .with_task_priority(priority)
    };
    let blocker = Arc::new(
        agent
            .session_async(
                workspace.path().to_string_lossy(),
                Some(make_session(
                    "scheduler-blocker",
                    crate::task_scheduler::TaskPriority::Foreground,
                )),
            )
            .await
            .unwrap(),
    );
    let background = Arc::new(
        agent
            .session_async(
                workspace.path().to_string_lossy(),
                Some(make_session(
                    "scheduler-background",
                    crate::task_scheduler::TaskPriority::Background,
                )),
            )
            .await
            .unwrap(),
    );
    let interactive = Arc::new(
        agent
            .session_async(
                workspace.path().to_string_lossy(),
                Some(make_session(
                    "scheduler-interactive",
                    crate::task_scheduler::TaskPriority::Interactive,
                )),
            )
            .await
            .unwrap(),
    );

    let blocker_run = tokio::spawn({
        let blocker = Arc::clone(&blocker);
        async move { blocker.send("blocker", None).await }
    });
    assert_eq!(started_rx.recv().await.as_deref(), Some("blocker"));

    let background_run = tokio::spawn({
        let background = Arc::clone(&background);
        async move { background.send("background", None).await }
    });
    while agent.task_scheduler_stats().await.unwrap().pending < 1 {
        tokio::task::yield_now().await;
    }
    let interactive_run = tokio::spawn({
        let interactive = Arc::clone(&interactive);
        async move { interactive.send("interactive", None).await }
    });
    while agent.task_scheduler_stats().await.unwrap().pending < 2 {
        tokio::task::yield_now().await;
    }
    let queued = agent.task_scheduler_stats().await.unwrap();
    assert_eq!(queued.pending_by_priority.background, 1);
    assert_eq!(queued.pending_by_priority.interactive, 1);

    release.add_permits(1);
    assert_eq!(started_rx.recv().await.as_deref(), Some("interactive"));
    release.add_permits(1);
    assert_eq!(started_rx.recv().await.as_deref(), Some("background"));
    release.add_permits(1);

    blocker_run.await.unwrap().unwrap();
    interactive_run.await.unwrap().unwrap();
    background_run.await.unwrap().unwrap();
    assert_eq!(agent.task_scheduler_stats().await.unwrap().active, 0);
    agent.close().await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn host_direct_tools_share_the_agent_scheduler_with_conversation_runs() {
    let workspace = tempfile::tempdir().unwrap();
    let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
    let release = Arc::new(tokio::sync::Semaphore::new(0));
    let client = Arc::new(TaskSchedulerProbeClient {
        started: started_tx,
        release: Arc::clone(&release),
    });
    let mut config = test_config();
    config.task_scheduler = crate::task_scheduler::TaskSchedulerConfig {
        max_active: 1,
        aging_interval_ms: 60_000,
    };
    config.memory = Some(crate::memory::MemoryConfig {
        llm_extraction: false,
        ..Default::default()
    });
    let agent = Agent::from_config(config).await.unwrap();
    let base_options = |id: &str| {
        SessionOptions::new()
            .with_session_id(id)
            .with_llm_client(client.clone())
            .with_planning_mode(crate::prompts::PlanningMode::Disabled)
            .with_continuation(false)
    };
    let conversation = Arc::new(
        agent
            .session_async(
                workspace.path().to_string_lossy(),
                Some(base_options("direct-scheduler-conversation")),
            )
            .await
            .unwrap(),
    );
    let direct = Arc::new(
        agent
            .session_async(
                workspace.path().to_string_lossy(),
                Some(base_options("direct-scheduler-tool")),
            )
            .await
            .unwrap(),
    );
    direct
        .register_dynamic_tool(Arc::new(NamedSessionTool("scheduled-direct".to_string())))
        .unwrap();

    let conversation_run = tokio::spawn({
        let conversation = Arc::clone(&conversation);
        async move { conversation.send("hold-global-slot", None).await }
    });
    assert_eq!(started_rx.recv().await.as_deref(), Some("hold-global-slot"));
    let direct_run = tokio::spawn({
        let direct = Arc::clone(&direct);
        async move { direct.tool("scheduled-direct", serde_json::json!({})).await }
    });
    while agent.task_scheduler_stats().await.unwrap().pending < 1 {
        tokio::task::yield_now().await;
    }
    assert!(!direct_run.is_finished());

    release.add_permits(1);
    conversation_run.await.unwrap().unwrap();
    assert_eq!(direct_run.await.unwrap().unwrap().output, "ok");
    assert_eq!(agent.task_scheduler_stats().await.unwrap().active, 0);
    agent.close().await;
}

#[tokio::test]
async fn test_session_default() {
    let agent = Agent::from_config(test_config()).await.unwrap();
    let session = agent.session_async("/tmp/test-workspace", None).await;
    assert!(session.is_ok());
    let session = session.unwrap();
    let debug = format!("{:?}", session);
    assert!(debug.contains("AgentSession"));
    let health = session.model_middleware_health();
    assert_eq!(health.trust_admitted, 0);
    assert_eq!(health.trust_rejected, 0);
    let encoded = serde_json::to_value(&health).expect("middleware health must serialize");
    assert_eq!(encoded["trustAdmitted"], 0);
    assert!(encoded.get("prompt").is_none());
    assert!(encoded.get("toolResult").is_none());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn concurrent_session_direct_generations_share_provider_admission() {
    let dir = tempfile::tempdir().unwrap();
    let client = Arc::new(SessionAdmissionClient::default());
    let agent = Agent::from_config(test_config()).await.unwrap();
    let session = agent
        .session_async(
            dir.path().to_string_lossy().to_string(),
            Some(SessionOptions::new().with_llm_client(client.clone())),
        )
        .await
        .unwrap();
    let args = serde_json::json!({
        "schema": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
                "ok": {"type": "boolean"}
            },
            "required": ["ok"]
        },
        "schema_name": "session_admission",
        "prompt": "Return an object whose ok field is true.",
        "mode": "prompt",
        "max_repair_attempts": 0,
        "timeout_ms": 2_000
    });

    let (first, second) = tokio::join!(
        session.tool("generate_object", args.clone()),
        session.tool("generate_object", args)
    );
    let results = [first.unwrap(), second.unwrap()];

    assert!(results
        .iter()
        .all(|result| result.exit_code == 0 && result.output.contains(r#""ok":true"#)));
    assert_eq!(
        client.max_active.load(std::sync::atomic::Ordering::SeqCst),
        1,
        "all host-direct loops in one session must share the provider gate"
    );
    let mut queue_waits = results
        .iter()
        .map(|result| {
            result
                .metadata
                .as_ref()
                .and_then(|metadata| metadata.pointer("/generation_admission/queue_wait_ms"))
                .and_then(serde_json::Value::as_u64)
                .expect("generation admission metadata")
        })
        .collect::<Vec<_>>();
    queue_waits.sort_unstable();
    assert!(
        queue_waits[1] >= 80,
        "the second direct generation should wait for session capacity: {queue_waits:?}"
    );
    let pool_health = session
        .model_generation_pool_health()
        .await
        .unwrap()
        .expect("the test client publishes a provider pool");
    let scheduler_health = pool_health
        .scheduler
        .expect("session provider admission is scheduler-bound");
    assert!(scheduler_health.observed);
    assert!(!scheduler_health.live);
    assert_eq!(scheduler_health.active, 0);
    assert_eq!(scheduler_health.pending, 0);
    assert_eq!(scheduler_health.admitted, 2);
    assert_eq!(scheduler_health.released, 2);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn independent_sessions_share_scheduler_backed_provider_generation_capacity() {
    let dir = tempfile::tempdir().unwrap();
    let client = Arc::new(SessionAdmissionClient::default());
    let agent = Agent::from_config(test_config()).await.unwrap();
    let options = |id: &str| {
        SessionOptions::new()
            .with_session_id(id)
            .with_llm_client(client.clone())
            .with_planning_mode(crate::prompts::PlanningMode::Disabled)
            .with_continuation(false)
    };
    let first = Arc::new(
        agent
            .session_async(dir.path().to_string_lossy(), Some(options("provider-a")))
            .await
            .unwrap(),
    );
    let second = Arc::new(
        agent
            .session_async(dir.path().to_string_lossy(), Some(options("provider-b")))
            .await
            .unwrap(),
    );
    let args = serde_json::json!({
        "schema": {
            "type": "object",
            "additionalProperties": false,
            "properties": {"ok": {"type": "boolean"}},
            "required": ["ok"]
        },
        "schema_name": "shared_provider",
        "prompt": "Return an object whose ok field is true.",
        "mode": "prompt",
        "max_repair_attempts": 0,
        "timeout_ms": 2_000
    });
    let (first_result, second_result) = tokio::join!(
        first.tool("generate_object", args.clone()),
        second.tool("generate_object", args)
    );
    assert!(first_result.unwrap().exit_code == 0);
    assert!(second_result.unwrap().exit_code == 0);
    assert_eq!(
        client.max_active.load(std::sync::atomic::Ordering::SeqCst),
        1,
        "sessions sharing one provider pool must not exceed its capacity"
    );
    let pool_health = first
        .model_generation_pool_health()
        .await
        .unwrap()
        .expect("the test client publishes a provider pool");
    let scheduler_health = pool_health
        .scheduler
        .expect("session provider admission is scheduler-bound");
    assert!(scheduler_health.observed);
    assert!(scheduler_health.admitted >= 2);
    assert!(scheduler_health.released >= 2);
    agent.close().await;
}