Agent turn orchestration and sessions

Updated Oct 09, 2026

Corbusier orchestrates agent turns through a single service path that validates the target backend, resolves or rotates a backend runtime session, executes the turn, and routes tool calls in deterministic order. Session continuity is tracked per (tenant_id, backend_id, conversation_id) until the session expires, with every repository query binding RequestContext.tenant_id so tenant isolation is enforced for reuse and rotation.

When a stored session is still active within the current tenant, it is reused. When expired, the session is marked expired and a new runtime session is created automatically. The session repository commits a reserved slot row for the current tenant before the runtime session is created, so expiry detection, replacement-session claiming, and subsequent reuse all remain tenant-scoped and happen at the database boundary before any external runtime call is made.

use std::sync::Arc;

use corbusier::agent_backend::{
    adapters::memory::{
        InMemoryAgentRuntime, InMemoryBackendRegistry, InMemoryToolRouter,
        InMemoryTurnSessionRepository,
    },
    domain::{BackendId, ToolCallRequest, TurnExecutionRequest, TurnExecutionResult},
    ports::BackendRegistryRepository,
    services::{
        AgentTurnOrchestratorPorts, AgentTurnOrchestratorService, ExecuteAgentTurnRequest,
    },
};
use chrono::Duration;
use corbusier::context::{CorrelationId, RequestContext, SessionId, TenantId, UserId};
use mockable::DefaultClock;
use serde_json::json;
use uuid::Uuid;

async fn execute_orchestrated_turn() -> Result<(), Box<dyn std::error::Error>> {
    let registry = Arc::new(InMemoryBackendRegistry::new());
    let sessions = Arc::new(InMemoryTurnSessionRepository::new());
    let runtime = Arc::new(InMemoryAgentRuntime::new());
    let router = Arc::new(InMemoryToolRouter::new());
    let clock = Arc::new(DefaultClock);
    let ctx = RequestContext::new(
        TenantId::new(),
        CorrelationId::new(),
        UserId::new(),
        SessionId::new(),
    );

    // Register one backend used for orchestration.
    let backend = corbusier::agent_backend::services::BackendRegistryService::new(
        registry.clone(),
        clock.clone(),
    )
    .register(
        &ctx,
        corbusier::agent_backend::services::RegisterBackendRequest::new(
            "claude_code_sdk",
            "Claude Code SDK",
            "1.0.0",
            "Anthropic",
        )
        .with_capabilities(true, true),
    )
    .await?;

    // Configure runtime and tool router behaviour for demonstration.
    runtime.queue_turn_result(TurnExecutionResult::new(
        "assistant response",
        vec![ToolCallRequest::new("search_docs", json!({"query": "roadmap"}))?],
    ))?;
    router.set_tool_response("search_docs", json!({"matches": 3}))?;

    let orchestrator = AgentTurnOrchestratorService::with_config(
        AgentTurnOrchestratorPorts {
            backend_registry: registry,
            turn_sessions: sessions,
            runtime,
            tool_router: router,
            clock,
        },
        corbusier::agent_backend::services::AgentTurnOrchestratorConfig::new(
            Duration::minutes(5),
        )?,
    );

    let response = orchestrator
        .execute_turn(
            &ctx,
            ExecuteAgentTurnRequest::new(
                BackendId::from_uuid(backend.id().into_inner()),
                TurnExecutionRequest::new(Uuid::new_v4(), "Please summarize this", Vec::new()),
            ),
        )
        .await?;

    assert_eq!(response.tool_results().len(), 1);
    assert!(!response.rotated_session());
    Ok(())
}

Build standard

Development builds (make test, make lint, make typecheck and the debug build) use the parallel rustc frontend (-Zthreads=8) and, on Linux, the mold linker. Install mold before building on Linux: the configuration names it, so a build without it fails at link time.

The flags live in .cargo/config.toml, but Cargo applies exactly one rustflags source and an assigned RUSTFLAGS replaces every configuration source. The Makefile therefore restates the flags in each recipe and keeps any RUSTFLAGS already in the environment, appending the standard flags after it. Two builds are held out deliberately. The coverage build assigns its own flags, because a measurement should not depend on the fast flags. make release keeps the environment's RUSTFLAGS and names neither fast flag, so a shipped artefact links with the platform linker. A bare cargo build --release takes the configuration's flags unless RUSTFLAGS is assigned, for example RUSTFLAGS="" cargo build --release.

Cranelift is the development-profile code generator, selected in .cargo/config.toml; coverage selects LLVM explicitly because instrumentation needs it. See ADR 012 for the reasoning.