github-copilot-sdk 1.0.32-preview.1

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
//! Observe lifecycle and event traffic without owning permission decisions.
//!
//! Demonstrates the channel-based observer APIs:
//!
//! - [`Client::subscribe_lifecycle`] — a [`LifecycleSubscription`] of
//!   every `session.lifecycle` notification (created / deleted / updated /
//!   foreground / background). Filter by matching on `event.event_type` in
//!   the consumer.
//! - [`Session::subscribe`] — an [`EventSubscription`] for the per-session
//!   `session.event` stream (assistant messages, tool calls, permission
//!   prompts, etc.).
//!   Observe-only — the constructor handler still owns permission decisions.
//! - [`Client::get_session_metadata`] — inspect a session without resuming
//!   it.
//! - [`Client::force_stop`] — synchronous shutdown for cleanup paths.
//!
//! Drop the receiver to unsubscribe — there is no separate cancel handle.
//! A slow consumer gets [`RecvErrorKind::Lagged`] with the number of skipped
//! events and keeps receiving on its next `recv`; it does not block the
//! producer.
//!
//! ```sh
//! cargo run -p github-copilot-sdk --example lifecycle_observer
//! ```
//!
//! [`Client::subscribe_lifecycle`]: github_copilot_sdk::Client::subscribe_lifecycle
//! [`LifecycleSubscription`]: github_copilot_sdk::LifecycleSubscription
//! [`Session::subscribe`]: github_copilot_sdk::session::Session::subscribe
//! [`EventSubscription`]: github_copilot_sdk::EventSubscription
//! [`Client::get_session_metadata`]: github_copilot_sdk::Client::get_session_metadata
//! [`Client::force_stop`]: github_copilot_sdk::Client::force_stop
//! [`RecvErrorKind::Lagged`]: github_copilot_sdk::subscription::RecvErrorKind::Lagged

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use github_copilot_sdk::handler::ApproveAllHandler;
use github_copilot_sdk::subscription::RecvErrorKind;
use github_copilot_sdk::types::{MessageOptions, SessionConfig, SessionLifecycleEventType};
use github_copilot_sdk::{Client, ClientOptions};

#[tokio::main]
async fn main() -> Result<(), github_copilot_sdk::Error> {
    let client = Client::start(ClientOptions::default()).await?;
    println!("[client] started, pid: {:?}", client.pid());

    // Wildcard lifecycle subscriber: see every session.lifecycle event,
    // counting deletions inline by filtering on event_type.
    let mut lifecycle_rx = client.subscribe_lifecycle();
    let deleted = Arc::new(AtomicUsize::new(0));
    let deleted_clone = Arc::clone(&deleted);
    let lifecycle_task = tokio::spawn(async move {
        loop {
            let event = match lifecycle_rx.recv().await {
                Ok(event) => event,
                // Fell behind: the skipped events are gone, keep receiving.
                Err(error) if matches!(error.kind(), RecvErrorKind::Lagged(_)) => continue,
                Err(_) => break,
            };
            let summary = event
                .metadata
                .as_ref()
                .and_then(|m| m.summary.as_deref())
                .unwrap_or("<no summary>");
            println!(
                "[lifecycle:*] {:?} session={} summary={}",
                event.event_type, event.session_id, summary,
            );
            if event.event_type == SessionLifecycleEventType::Deleted {
                deleted_clone.fetch_add(1, Ordering::Relaxed);
            }
        }
    });

    let config = SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler));
    let session = client.create_session(config).await?;
    println!("[client] session created: {}", session.id());

    // Per-session observer: see every assistant message, tool call, etc.
    // Subscribers fire alongside the constructor handler; they're great for
    // logging or metrics that should run regardless of how the handler
    // decides to respond.
    let mut session_rx = session.subscribe();
    let session_events = Arc::new(AtomicUsize::new(0));
    let session_events_clone = Arc::clone(&session_events);
    let session_task = tokio::spawn(async move {
        loop {
            let event = match session_rx.recv().await {
                Ok(event) => event,
                Err(error) if matches!(error.kind(), RecvErrorKind::Lagged(_)) => continue,
                Err(_) => break,
            };
            session_events_clone.fetch_add(1, Ordering::Relaxed);
            println!("[session-event] {}", event.event_type);
        }
    });

    if let Some(metadata) = client.get_session_metadata(session.id()).await? {
        println!(
            "[metadata] id={} modified={} summary={}",
            metadata.session_id,
            metadata.modified_time,
            metadata.summary.as_deref().unwrap_or("<no summary>"),
        );
    }

    session
        .send_and_wait(
            MessageOptions::new("Say hello in five words or fewer.")
                .with_wait_timeout(Duration::from_secs(60)),
        )
        .await?;

    session.disconnect().await?;

    // Synchronous shutdown — useful in panicking-cleanup paths or tests
    // where you don't have an async runtime available to await `stop()`.
    // For graceful shutdown in normal flow, prefer `client.stop().await`.
    client.force_stop();
    println!("[client] force-stopped");

    // Stopping the client closes the broadcast senders, so the consumer
    // tasks observe `RecvError::Closed` and exit cleanly.
    let _ = lifecycle_task.await;
    let _ = session_task.await;

    println!(
        "\n[summary] session_events={} sessions_deleted={}",
        session_events.load(Ordering::Relaxed),
        deleted.load(Ordering::Relaxed),
    );

    Ok(())
}