github-copilot-sdk 1.0.13

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
Documentation
use std::sync::Arc;

use async_trait::async_trait;
use github_copilot_sdk::handler::ApproveAllHandler;
use github_copilot_sdk::tool::ToolHandler;
use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult};
use serde_json::json;
use tokio::sync::{Mutex, mpsc, oneshot};
use tokio::time::{Duration, timeout};

use super::support::DEFAULT_TEST_TOKEN;

#[tokio::test]
async fn should_cancel_tool_handler_when_session_disconnects() {
    super::support::with_dedicated_e2e_context(
        "external_tool_cancellation",
        "should_cancel_tool_handler_when_session_disconnects",
        |ctx| {
            Box::pin(async move {
                ctx.set_default_copilot_user();
                let client = ctx.start_client().await;
                let (started_tx, mut started_rx) = mpsc::unbounded_channel();
                let (release_tx, release_rx) = oneshot::channel();
                let (cancelled_tx, cancelled_rx) = oneshot::channel();
                let tool = Arc::new(CancelAwareSlowTool {
                    started_tx,
                    release_rx: Mutex::new(Some(release_rx)),
                    cancelled_tx: Mutex::new(Some(cancelled_tx)),
                });

                let session = client
                    .create_session(
                        SessionConfig::default()
                            .with_github_token(DEFAULT_TEST_TOKEN)
                            .with_permission_handler(Arc::new(ApproveAllHandler))
                            .with_tools(vec![
                                Tool::new("slow_analysis")
                                    .with_description(
                                        "A slow analysis tool that blocks until released",
                                    )
                                    .with_parameters(json!({
                                        "type": "object",
                                        "properties": {
                                            "value": {
                                                "type": "string",
                                                "description": "Value to analyze"
                                            }
                                        },
                                        "required": ["value"]
                                    }))
                                    .with_handler(tool),
                            ]),
                    )
                    .await
                    .expect("create session");

                session
                    .send("Use slow_analysis with value 'test_abort'. Wait for the result.")
                    .await
                    .expect("send tool turn");

                let started_value = timeout(Duration::from_secs(60), started_rx.recv())
                    .await
                    .expect("tool start wait timed out")
                    .expect("tool start channel closed");
                assert_eq!(started_value, "test_abort");

                session.disconnect().await.expect("disconnect session");
                timeout(Duration::from_secs(60), cancelled_rx)
                    .await
                    .expect("tool cancellation wait timed out")
                    .expect("tool cancellation sender dropped");

                let _ = release_tx.send("RELEASED".to_string());
                client.stop().await.expect("stop client");
            })
        },
    )
    .await;
}

struct CancelAwareSlowTool {
    started_tx: mpsc::UnboundedSender<String>,
    release_rx: Mutex<Option<oneshot::Receiver<String>>>,
    cancelled_tx: Mutex<Option<oneshot::Sender<()>>>,
}

struct CancelSignalGuard {
    cancelled_tx: Option<oneshot::Sender<()>>,
}

impl Drop for CancelSignalGuard {
    fn drop(&mut self) {
        if let Some(sender) = self.cancelled_tx.take() {
            let _ = sender.send(());
        }
    }
}

#[async_trait]
impl ToolHandler for CancelAwareSlowTool {
    async fn call(&self, invocation: ToolInvocation) -> Result<ToolResult, Error> {
        let value = invocation
            .arguments
            .get("value")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default()
            .to_string();
        let _ = self.started_tx.send(value);

        let cancelled_tx = self.cancelled_tx.lock().await.take();
        let _guard = CancelSignalGuard { cancelled_tx };

        let release_rx = self
            .release_rx
            .lock()
            .await
            .take()
            .expect("slow tool called once");
        let released = release_rx.await.unwrap_or_else(|_| "released".to_string());
        Ok(ToolResult::Text(released))
    }
}