langshell 0.2.1

Rust SDK for building stateful, capability-scoped LangShell runtimes.
Documentation
use std::sync::Mutex;

use langshell::{
    ErrorObject, LangShell, Language, LanguageRuntime, Metrics, RunRequest, RunResult, RunStatus,
    RuntimeFuture, SessionId, SessionLimits, ToolRegistry,
};
use serde_json::json;

#[derive(Debug)]
struct FakeRuntime {
    language: Language,
    sessions: Mutex<Vec<SessionId>>,
}

impl FakeRuntime {
    fn new(_registry: ToolRegistry, _limits: SessionLimits) -> Self {
        Self {
            language: Language::Python,
            sessions: Mutex::new(Vec::new()),
        }
    }
}

impl LanguageRuntime for FakeRuntime {
    fn language(&self) -> Language {
        self.language
    }

    fn create_session(
        &self,
        session_id: SessionId,
        _limits: Option<SessionLimits>,
    ) -> RuntimeFuture<'_, Result<(), ErrorObject>> {
        let result = self
            .sessions
            .lock()
            .map(|mut sessions| sessions.push(session_id))
            .map_err(|err| ErrorObject::new("RUNTIME_ERROR", err.to_string()));
        Box::pin(async move { result })
    }

    fn run(&self, request: RunRequest) -> RuntimeFuture<'_, RunResult> {
        let result = RunResult::ok(
            Some(json!({
                "language": language_name(request.language),
                "code": request.code,
            })),
            String::new(),
            Metrics::default(),
        );
        Box::pin(async move { result })
    }

    fn destroy_session(
        &self,
        session_id: SessionId,
    ) -> RuntimeFuture<'_, Result<bool, ErrorObject>> {
        let result = self
            .sessions
            .lock()
            .map(|mut sessions| {
                let before = sessions.len();
                sessions.retain(|id| id != &session_id);
                sessions.len() != before
            })
            .map_err(|err| ErrorObject::new("RUNTIME_ERROR", err.to_string()));
        Box::pin(async move { result })
    }

    fn list_sessions(&self) -> RuntimeFuture<'_, Result<Vec<SessionId>, ErrorObject>> {
        let result = self
            .sessions
            .lock()
            .map(|sessions| sessions.clone())
            .map_err(|err| ErrorObject::new("RUNTIME_ERROR", err.to_string()));
        Box::pin(async move { result })
    }

    fn snapshot_session(
        &self,
        session_id: SessionId,
    ) -> RuntimeFuture<'_, Result<Vec<u8>, ErrorObject>> {
        let snapshot = format!("fake-runtime:{}", session_id.0).into_bytes();
        Box::pin(async move { Ok(snapshot) })
    }

    fn restore_session(
        &self,
        _snapshot: Vec<u8>,
        session_id: Option<SessionId>,
    ) -> RuntimeFuture<'_, Result<SessionId, ErrorObject>> {
        let session_id = session_id.unwrap_or_else(|| SessionId("restored".to_owned()));
        let result = self
            .sessions
            .lock()
            .map(|mut sessions| {
                sessions.push(session_id.clone());
                session_id
            })
            .map_err(|err| ErrorObject::new("RUNTIME_ERROR", err.to_string()));
        Box::pin(async move { result })
    }

    fn can_restore_snapshot(&self, snapshot: &[u8]) -> bool {
        snapshot.starts_with(b"fake-runtime:")
    }
}

#[test]
fn builder_requires_runtime() {
    let error = LangShell::builder().build().expect_err("build should fail");
    assert_eq!(error.code, "INVALID_ARGUMENT");
}

#[test]
fn builder_rejects_duplicate_runtime_languages() {
    let error = LangShell::builder()
        .runtime(FakeRuntime::new)
        .runtime(FakeRuntime::new)
        .build()
        .expect_err("duplicate runtime should fail");
    assert_eq!(error.code, "INVALID_ARGUMENT");
}

#[tokio::test]
async fn sdk_delegates_to_registered_runtime() {
    let shell = LangShell::builder()
        .runtime(FakeRuntime::new)
        .build()
        .unwrap();

    let result = shell.session("sdk-e2e").run("result = 1").execute().await;

    assert_eq!(result.status, RunStatus::Ok, "{result:?}");
    assert_eq!(
        result.result,
        Some(json!({"language": "python", "code": "result = 1"}))
    );
}

#[tokio::test]
async fn missing_runtime_returns_validation_error() {
    let shell = LangShell::builder()
        .runtime(FakeRuntime::new)
        .build()
        .unwrap();
    let mut request = RunRequest::new("sdk-e2e", "result = 1").unwrap();
    request.language = Language::TypeScript;

    let result = shell.run(request).await;

    assert_eq!(result.status, RunStatus::ValidationError, "{result:?}");
    assert_eq!(result.error.unwrap().code, "UNSUPPORTED_FEATURE");
}

fn language_name(language: Language) -> &'static str {
    match language {
        Language::Python => "python",
        Language::TypeScript => "typescript",
    }
}