github-copilot-sdk 1.0.0-beta.7

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC. Technical preview, pre-1.0.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
# GitHub Copilot CLI SDK for Rust

A Rust SDK for programmatic access to the GitHub Copilot CLI.

> **Note:** This SDK is in technical preview and may change in breaking ways.

See [github/copilot-sdk](https://github.com/github/copilot-sdk) for the equivalent SDKs in TypeScript, Python, Go, and .NET. The Rust SDK seeks parity with those SDKs; see [Differences From Other SDKs](#differences-from-other-sdks) below for the small set of intentional divergences.

**Releases:** [github.com/github/copilot-sdk/releases?q=rust%2F](https://github.com/github/copilot-sdk/releases?q=rust%2F) — per-version release notes for the Rust crate.

## Quick Start

```rust,no_run
use std::sync::Arc;
use github_copilot_sdk::{Client, ClientOptions, SessionConfig};
use github_copilot_sdk::handler::ApproveAllHandler;

# async fn example() -> Result<(), github_copilot_sdk::Error> {
let client = Client::start(ClientOptions::default()).await?;
let session = client.create_session(
    SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)),
).await?;
let _message_id = session.send("Hello!").await?;
session.disconnect().await?;
client.stop().await.ok();
# Ok(())
# }
```

## Architecture

```text
Your Application
  github_copilot_sdk::Client  (manages CLI process lifecycle)
  github_copilot_sdk::Session (per-session event loop + handler dispatch)
       ↓ JSON-RPC over stdio or TCP
  copilot --server --stdio
```

The SDK manages the CLI process lifecycle: spawning, health-checking, and graceful shutdown. Communication uses [JSON-RPC 2.0](https://www.jsonrpc.org/specification) over stdin/stdout with `Content-Length` framing (the same protocol used by LSP). TCP transport is also supported.

## API Reference

### Client

```rust,ignore
// Start a client (spawns CLI process)
let client = Client::start(options).await?;

// Create a new session
let session = client.create_session(config.with_permission_handler(handler)).await?;

// Resume an existing session
let session = client.resume_session(config.with_permission_handler(handler)).await?;

// Low-level RPC
let result = client.call("method.name", Some(params)).await?;
let response = client.send_request("method.name", Some(params)).await?;

// Health check (echoes message back, returns typed PingResponse)
let pong = client.ping("hello").await?;

// Shutdown
client.stop().await?;
```

**`ClientOptions`:**

| Field         | Type                        | Description                                                     |
| ------------- | --------------------------- | --------------------------------------------------------------- |
| `program`     | `CliProgram`                | `Resolve` (default: auto-detect) or `Path(PathBuf)` (explicit)  |
| `prefix_args` | `Vec<OsString>`             | Args before `--server` (e.g. script path for node)              |
| `cwd`         | `PathBuf`                   | Working directory for CLI process                               |
| `env`         | `Vec<(OsString, OsString)>` | Environment variables for CLI process                           |
| `env_remove`  | `Vec<OsString>`             | Environment variables to remove                                 |
| `extra_args`  | `Vec<String>`               | Extra CLI flags                                                 |
| `transport`   | `Transport`                 | `Stdio` (default), `Tcp { port }`, or `External { host, port }` |

With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`.

### Session

Created via `Client::create_session` or `Client::resume_session`. Owns an internal event loop that dispatches CLI callbacks to the focused handler traits you install on `SessionConfig`, and broadcasts session events through `subscribe()`.

```rust,ignore
use github_copilot_sdk::MessageOptions;

// Simple send — &str / String convert into MessageOptions automatically.
// Returns the assigned message ID for correlation with later events.
let _id = session.send("Fix the bug in auth.rs").await?;

// Send with mode and attachments
let _id = session
    .send(
        MessageOptions::new("What's in this image?")
            .with_mode("autopilot")
            .with_attachments(attachments),
    )
    .await?;

// Message history
let messages = session.get_events().await?;

// Abort the current agent turn
session.abort().await?;

// Model management
session.set_model("claude-sonnet-4.5", None).await?;

// Generated typed RPCs cover lower-level session operations.
let model = session.rpc().model().get_current().await?;
let mode = session.rpc().mode().get().await?;

// Workspace files
let files = session.rpc().workspaces().list_files().await?;
let content = session
    .rpc()
    .workspaces()
    .read_file(github_copilot_sdk::generated::api_types::WorkspacesReadFileRequest {
        path: "plan.md".to_string(),
    })
    .await?;

// Plan management
let plan = session.rpc().plan().read().await?;
session
    .rpc()
    .plan()
    .update(github_copilot_sdk::generated::api_types::PlanUpdateRequest {
        content: "Updated plan content".to_string(),
    })
    .await?;

// Fleet (sub-agents)
session
    .rpc()
    .fleet()
    .start(github_copilot_sdk::generated::api_types::FleetStartRequest {
        prompt: Some("Implement the auth module".to_string()),
    })
    .await?;

// Cleanup (preserves on-disk session state for later resume)
session.disconnect().await?;
```

#### Typed RPC namespace

High-level helpers are convenience wrappers over a fully-typed
JSON-RPC namespace generated from the GitHub Copilot CLI schema. `Client::rpc()`
and `Session::rpc()` give direct access to every method on the wire,
including ones with no helper today, with strongly-typed request and
response structs.

```rust,ignore
// Common generated RPCs.
let files = session.rpc().workspaces().list_files().await?.files;
let models = client.rpc().models().list().await?.models;

// Methods with no helper — full schema-typed access.
let agents = session.rpc().agent().list().await?.agents;
let tasks = session.rpc().tasks().list().await?.tasks;
let forked = client
    .rpc()
    .sessions()
    .fork(github_copilot_sdk::generated::api_types::SessionsForkRequest {
        session_id: "session-id".into(),
        to_event_id: None,
    })
    .await?;
```

New RPCs land in the namespace immediately as the schema regenerates;
helpers are added on top only when an ergonomic story is worth the
maintenance.

### Handler Traits

The SDK exposes five focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method:

| Trait                   | Setter                            | Purpose                                       |
| ----------------------- | --------------------------------- | --------------------------------------------- |
| `PermissionHandler`     | `with_permission_handler(...)`    | Approve/deny tool-use permission requests     |
| `ElicitationHandler`    | `with_elicitation_handler(...)`   | Respond to structured elicitation prompts     |
| `UserInputHandler`      | `with_user_input_handler(...)`    | Answer free-form / choice user-input prompts  |
| `ExitPlanModeHandler`   | `with_exit_plan_mode_handler(...)`| Respond when the agent exits plan mode        |
| `AutoModeSwitchHandler` | `with_auto_mode_switch_handler(...)`| Respond to automatic mode-switch proposals  |

The CLI's `requestPermission` / `requestElicitation` / `requestUserInput` / etc. wire flags are derived automatically from which traits you've installed — clients that don't install a handler are silently skipped, letting another connected client handle the request.

```rust,ignore
use std::sync::Arc;
use async_trait::async_trait;
use github_copilot_sdk::handler::{PermissionHandler, PermissionResult};
use github_copilot_sdk::types::{PermissionRequestData, RequestId, SessionId};

struct MyPermissions;

#[async_trait]
impl PermissionHandler for MyPermissions {
    async fn handle(
        &self,
        _sid: SessionId,
        _rid: RequestId,
        data: PermissionRequestData,
    ) -> PermissionResult {
        if data.extra.get("tool").and_then(|v| v.as_str()) == Some("view") {
            PermissionResult::approve_once()
        } else {
            PermissionResult::reject(None)
        }
    }
}

let config = SessionConfig::default().with_permission_handler(Arc::new(MyPermissions));
```

A single type can implement multiple handler traits — share one `Arc<Self>` across the setters by cloning:

```rust,ignore
let h = Arc::new(MyHandler);
let config = SessionConfig::default()
    .with_permission_handler(h.clone())
    .with_user_input_handler(h);
```

The built-in `ApproveAllHandler` and `DenyAllHandler` implement `PermissionHandler` for the common cases. To observe streamed session events (assistant messages, tool calls, etc.), call `session.subscribe()` — see [Streaming](#streaming) below.

### SessionConfig

```rust,ignore
let config = SessionConfig {
    model: Some("gpt-5".into()),
    system_message: Some(SystemMessageConfig {
        content: Some("Always explain your reasoning.".into()),
        ..Default::default()
    }),
    ..Default::default()
}
.with_elicitation_handler(Arc::new(my_elicitation_handler))
.with_permission_handler(handler);
let session = client.create_session(config).await?;
```

### Session Hooks

Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a `SessionHooks` impl with [`SessionConfig::with_hooks`] — the SDK auto-enables `hooks` in `SessionConfig` when one is set.

```rust,ignore
use std::sync::Arc;
use github_copilot_sdk::hooks::*;
use async_trait::async_trait;

struct MyHooks;

#[async_trait]
impl SessionHooks for MyHooks {
    async fn on_hook(&self, event: HookEvent) -> HookOutput {
        match event {
            HookEvent::PreToolUse { input, ctx } => {
                if input.tool_name == "dangerous_tool" {
                    HookOutput::PreToolUse(PreToolUseOutput {
                        permission_decision: Some("deny".to_string()),
                        permission_decision_reason: Some("blocked by policy".to_string()),
                        ..Default::default()
                    })
                } else {
                    HookOutput::None // pass through
                }
            }
            HookEvent::SessionStart { input, .. } => {
                HookOutput::SessionStart(SessionStartOutput {
                    additional_context: Some("Extra system context".to_string()),
                    ..Default::default()
                })
            }
            _ => HookOutput::None,
        }
    }
}

let session = client
    .create_session(
        config
            .with_permission_handler(handler)
            .with_hooks(Arc::new(MyHooks)),
    )
    .await?;
```

**Hook events:** `PreToolUse`, `PostToolUse`, `UserPromptSubmitted`, `SessionStart`, `SessionEnd`, `ErrorOccurred`. Each carries typed input/output structs. Return `HookOutput::None` for events you don't handle.

### System Message Transforms

Transforms customize system message sections during session creation. The SDK injects `action: "transform"` entries for each section ID your transform handles.

```rust,ignore
use github_copilot_sdk::transforms::*;
use async_trait::async_trait;

struct MyTransform;

#[async_trait]
impl SystemMessageTransform for MyTransform {
    fn section_ids(&self) -> Vec<String> {
        vec!["instructions".to_string()]
    }

    async fn transform_section(
        &self,
        _section_id: &str,
        content: &str,
        _ctx: TransformContext,
    ) -> Option<String> {
        Some(format!("{content}\n\nAlways be concise."))
    }
}

let session = client
    .create_session(
        config
            .with_permission_handler(handler)
            .with_system_message_transform(Arc::new(MyTransform)),
    )
    .await?;
```

### Tool Registration

Define client-side tools as named types implementing `ToolHandler` and attach
them to `Tool` declarations via `Tool::with_handler`, then install via
`SessionConfig::with_tools`. Enable the `derive` feature for `schema_for::<T>()`
— it generates JSON Schema from Rust types via `schemars`.

```rust,ignore
use std::sync::Arc;
use github_copilot_sdk::handler::ApproveAllHandler;
use github_copilot_sdk::tool::{schema_for, JsonSchema, ToolHandler};
use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult};
use serde::Deserialize;
use async_trait::async_trait;

#[derive(Deserialize, JsonSchema)]
struct GetWeatherParams {
    /// City name
    city: String,
    /// Temperature unit
    unit: Option<String>,
}

struct GetWeatherTool;

#[async_trait]
impl ToolHandler for GetWeatherTool {
    async fn call(&self, inv: ToolInvocation) -> Result<ToolResult, Error> {
        let params: GetWeatherParams = serde_json::from_value(inv.arguments)?;
        Ok(ToolResult::Text(format!("Weather in {}: sunny", params.city)))
    }
}

let tool = Tool::new("get_weather")
    .with_description("Get weather for a city")
    .with_parameters(schema_for::<GetWeatherParams>())
    .with_handler(Arc::new(GetWeatherTool));

let config = SessionConfig::default()
    .with_permission_handler(Arc::new(ApproveAllHandler))
    .with_tools(vec![tool]);
let session = client.create_session(config).await?;
```

Tools are named types (not closures) — visible in stack traces and navigable via "go to definition". The SDK registers each tool's handler under its `Tool::name` and surfaces the same `Tool` definitions to the CLI automatically.

Tools without an attached handler (`Tool::with_handler` never called) are declaration-only: the SDK advertises them on the wire but doesn't dispatch invocations to anything. Useful when another connected client services the tool.

For trivial tools that don't need a named type, the `define_tool` helper function (available with the `derive` feature) collapses the definition to a single expression and returns a fully-formed `Tool` with handler attached:

```rust,ignore
use github_copilot_sdk::tool::{define_tool, JsonSchema};
use github_copilot_sdk::ToolResult;
use serde::Deserialize;

#[derive(Deserialize, JsonSchema)]
struct GetWeatherParams { city: String }

let tool = define_tool(
    "get_weather",
    "Get weather for a city",
    |_inv, params: GetWeatherParams| async move {
        Ok(ToolResult::Text(format!("Sunny in {}", params.city)))
    },
);

let config = SessionConfig::default()
    .with_permission_handler(Arc::new(ApproveAllHandler))
    .with_tools(vec![tool]);
```

The closure receives the full [`ToolInvocation`](crate::types::ToolInvocation) alongside the deserialized parameters, so handlers that need `inv.session_id` or `inv.tool_call_id` for telemetry, streaming updates, or scoped lookups can use them directly. Use `_inv` when you don't need the metadata.

Reach for the `ToolHandler` trait directly when you need shared state across multiple methods or want a named type that shows up by name in stack traces.

### Permission Policies

Set a permission policy directly on `SessionConfig` with the chainable builders. They install a synthesized `PermissionHandler` so only permission requests are intercepted; every other event flows through unchanged.

```rust,ignore
let session = client
    .create_session(
        SessionConfig::default()
            .approve_all_permissions(),
        // or .deny_all_permissions()
        // or .approve_permissions_if(|data| {
        //     data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
        // })
    )
    .await?;
```

> The policy builders set the permission handler slot directly; they're equivalent to calling `with_permission_handler(...)` with the corresponding built-in (`ApproveAllHandler`, `DenyAllHandler`, or `permission::approve_if(...)`).

The `permission` module also exposes the policy primitives as standalone helpers for the rare case where you want to construct the handler value separately and install it via `with_permission_handler`:

```rust,ignore
use github_copilot_sdk::permission;

let handler = permission::approve_if(|data| {
    data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
});
// or permission::approve_all() / permission::deny_all()

let session = client
    .create_session(config.with_permission_handler(handler))
    .await?;
```

### Elicitation

To opt your client into receiving `elicitation.requested` broadcasts, install an `ElicitationHandler` on the session config. The wire flag `requestElicitation` is derived from the presence of the handler; clients without one are silently skipped, allowing other connected clients on the same CLI to handle the request.

```rust,ignore
use async_trait::async_trait;
use github_copilot_sdk::handler::{ElicitationHandler, ElicitationResult};
use github_copilot_sdk::types::{ElicitationRequest, RequestId, SessionId};

struct MyElicitation;

#[async_trait]
impl ElicitationHandler for MyElicitation {
    async fn handle(
        &self,
        _sid: SessionId,
        _rid: RequestId,
        _request: ElicitationRequest,
    ) -> ElicitationResult {
        ElicitationResult::cancel()
    }
}

let config = SessionConfig::default()
    .with_permission_handler(Arc::new(ApproveAllHandler))
    .with_elicitation_handler(Arc::new(MyElicitation));
```

The handler receives a message, optional JSON Schema for form fields, and an optional mode. Known modes include `Form` and `Url`, but the mode may be absent or an unknown future value.

### User Input Requests

Some sessions ask the user free-form questions (or multiple-choice prompts) outside the elicitation flow. Install a `UserInputHandler` and the SDK will forward `userInput.request` callbacks:

```rust,ignore
use async_trait::async_trait;
use github_copilot_sdk::handler::{UserInputHandler, UserInputResponse};
use github_copilot_sdk::types::SessionId;

struct MyUserInput;

#[async_trait]
impl UserInputHandler for MyUserInput {
    async fn handle(
        &self,
        _sid: SessionId,
        question: String,
        _choices: Option<Vec<String>>,
        _allow_freeform: Option<bool>,
    ) -> Option<UserInputResponse> {
        // Render `question` + `choices` to your UI, then:
        Some(UserInputResponse {
            answer: "Yes".to_string(),
            was_freeform: false,
        })
    }
}

let config = SessionConfig::default()
    .with_user_input_handler(Arc::new(MyUserInput));
```

Return `None` to signal "no answer available" (the CLI falls back to its own prompt).

### Slash Commands

Register named commands so users can invoke them as `/name args` from the TUI:

```rust,ignore
use github_copilot_sdk::types::{CommandContext, CommandDefinition, CommandHandler};
use async_trait::async_trait;

struct DeployCommand;

#[async_trait]
impl CommandHandler for DeployCommand {
    async fn on_command(&self, ctx: CommandContext) -> Result<(), github_copilot_sdk::Error> {
        println!("deploy {}", ctx.args);
        Ok(())
    }
}

let mut config = SessionConfig::default();
config.commands = Some(vec![
    CommandDefinition::new("deploy", Arc::new(DeployCommand))
        .with_description("Deploy the application"),
]);
```

Only `name` and `description` are sent over the wire; the handler stays in your process. Returning `Err(_)` surfaces the message back through the TUI.

### Streaming

Set `streaming: true` to receive incremental delta events alongside finalized messages:

```rust,ignore
let mut config = SessionConfig::default();
config.streaming = Some(true);

let mut events = session.subscribe();
while let Ok(event) = events.recv().await {
    match event.event_type.as_str() {
        "assistant.message_delta" | "assistant.reasoning_delta" => {
            if let Some(d) = event.data.get("delta").and_then(|v| v.as_str()) {
                print!("{d}");
            }
        }
        "assistant.message" => println!(),  // final
        _ => {}
    }
}
```

When streaming is off (the default), only the final `assistant.message` and `assistant.reasoning` events fire. Delta events arrive in order; concatenating their `delta` text payloads reproduces the final message.

### Infinite Sessions

Enable the SDK's session-store integration so conversations persist across CLI restarts and grow beyond the model's context window via automatic compaction:

```rust,ignore
use github_copilot_sdk::types::InfiniteSessionConfig;

let mut infinite = InfiniteSessionConfig::default();
infinite.workspace_path = Some("/path/to/workspace".into());

let mut config = SessionConfig::default();
config.infinite_sessions = Some(infinite);
```

The CLI emits `session.compaction_start` / `session.compaction_complete` events around each compaction. The session id remains stable across compactions; resume with `Client::resume_session` to pick up a prior conversation. Workspace state lives under `~/.copilot/session-state/{sessionId}` by default — override with `workspace_path` to relocate.

### Custom Providers (BYOK)

Route model traffic through your own inference endpoint instead of GitHub's hosted models:

```rust,ignore
use github_copilot_sdk::types::ProviderConfig;

let mut provider = ProviderConfig::default();
provider.provider_type = Some("openai".to_string());
provider.base_url = "https://my-proxy.example.com/v1".to_string();
provider.bearer_token = Some(std::env::var("OPENAI_API_KEY")?);

let mut config = SessionConfig::default();
config.provider = Some(provider);
```

Provider types include `"openai"`, `"azure"`, and `"anthropic"`. Set `wire_api` to `"completions"` or `"responses"` (OpenAI/Azure only). Custom headers go in `provider.headers`. The SDK forwards the configuration to the CLI verbatim — the CLI handles the upstream call, including authentication.

### Telemetry

Forward OpenTelemetry signals from the spawned CLI process to your collector:

```rust,ignore
use github_copilot_sdk::{ClientOptions, OtelExporterType, TelemetryConfig};

let mut telem = TelemetryConfig::default();
telem.exporter_type = Some(OtelExporterType::OtlpHttp);
telem.otlp_endpoint = Some("http://localhost:4318".to_string());
telem.source_name = Some("my-app".to_string());

let mut opts = ClientOptions::default();
opts.telemetry = Some(telem);
let client = Client::start(opts).await?;
```

The SDK injects the appropriate environment variables (`COPILOT_OTEL_EXPORTER_TYPE`, `OTEL_EXPORTER_OTLP_ENDPOINT`, ...) into the spawned CLI process. The SDK takes no OpenTelemetry dependency; the CLI itself owns the exporter pipeline. Caller-supplied `ClientOptions::env` entries override telemetry-injected values.

### Progress Reporting (`send_and_wait`)

For fire-and-forget messaging where you need to block until the agent finishes:

```rust,ignore
use std::time::Duration;
use github_copilot_sdk::MessageOptions;

// Sends a message and blocks until session.idle or session.error
session
    .send_and_wait(
        MessageOptions::new("Fix the bug").with_wait_timeout(Duration::from_secs(120)),
    )
    .await?;
```

Default timeout is 60 seconds. Only one `send_and_wait` can be active per session — concurrent calls return an error.

### Newtypes

**`SessionId`** — a newtype wrapper around `String` that prevents accidentally passing workspace IDs or request IDs where session IDs are expected. Transparent serialization (`#[serde(transparent)]`), zero-cost `Deref<Target=str>`, and ergonomic comparisons with `&str` and `String`.

```rust,ignore
use github_copilot_sdk::SessionId;

let id = SessionId::new("sess-abc123");
assert_eq!(id, "sess-abc123");           // compare with &str
let raw: String = id.into_inner();       // unwrap when needed
```

### Error Handling

The SDK uses a typed error enum:

```rust,ignore
pub enum Error {
    Protocol(ProtocolError),       // JSON-RPC framing, CLI startup, version mismatch
    Rpc { code: i32, message: String }, // CLI returned an error response
    Session(SessionError),         // Session not found, agent error, timeout, conflicts
    Io(std::io::Error),            // Transport I/O error
    Json(serde_json::Error),       // Serialization error
    BinaryNotFound { name, hint }, // CLI binary not found
}

// Check if the transport is broken (caller should discard the client)
if err.is_transport_failure() {
    client = Client::start(options).await?;
}
```

## Differences From Other SDKs

The Rust SDK aligns closely with the Node, Python, Go, and .NET SDKs but diverges
in a few places where Rust idiom or the type system gives a clearly better
shape, and exposes a small additional surface where the language affords
ergonomics the dynamically-typed SDKs don't.

### Shape divergence

- **`SessionFsProvider` registration is direct, not factory-closure.** Where
  Node/Python/Go/.NET accept a closure that the runtime calls on each
  session-create to build a fresh provider, the Rust SDK takes
  `Arc<dyn SessionFsProvider>` directly via
  [`SessionConfig::with_session_fs_provider`]. The factory pattern doesn't
  cleanly express in Rust at the session-config call site — there is no
  `Session` value to thread in, and the SDK already prefers traits over
  boxed closures for handler-shaped APIs (`PermissionHandler`, `ToolHandler`,
  `SessionHooks`,
  `SystemMessageTransform`).

```rust,ignore
use std::sync::Arc;
use github_copilot_sdk::session_fs::{SessionFsConfig, SessionFsConventions};

let mut options = ClientOptions::default();
options.session_fs = Some(SessionFsConfig::new(
    "/workspace",
    "/workspace/.copilot",
    SessionFsConventions::Posix,
));
let client = Client::start(options).await?;

let session = client
    .create_session(
        SessionConfig::default()
            .with_permission_handler(Arc::new(ApproveAllHandler))
            .with_session_fs_provider(Arc::new(MyProvider::new())),
    )
    .await?;
```

See [`examples/session_fs.rs`](examples/session_fs.rs) for a complete
in-memory provider implementation.

- **Canvas action dispatch is a single trait method, not per-action closures.**
  The Node SDK binds an optional `handler` closure on each entry of a canvas's
  `actions[]`. The Rust SDK exposes
  [`CanvasHandler::on_action`]crate::canvas::CanvasHandler::on_action and expects the implementor to match on
  `ctx.action_name`. Same reasoning as `SessionFsProvider`: per-callback
  `Box<dyn Fn>` fields fight `Send + Sync + 'static` and skip exhaustiveness
  checks, and the SDK prefers trait + default-impl methods for handler-shaped
  extension points.

### Rust-only API

A handful of conveniences exist only on the Rust SDK as of 0.1.0. These
are surface areas where Rust idiom (newtypes, enums, trait objects)
gives a clearly nicer shape than Node/Python/Go/.NET currently expose. Rust
gets to be Rust here — cross-SDK parity for these is a post-release
conversation, not a release blocker. None of these are deprecated and
none of them are scheduled for removal.

- **Typed newtypes**`SessionId` and `RequestId` are `#[serde(transparent)]`
  newtypes around `String`, so the type system distinguishes a session
  identifier from an arbitrary `String` at compile time. Node/Python/Go
  use bare strings.
- **Permission policy builders**`permission::approve_all`,
  `permission::deny_all`, and `permission::approve_if(predicate)`
  in `crate::permission` provide composable, no-handler-needed
  `PermissionHandler` shortcuts. Other SDKs require a
  full handler implementation for these patterns.
- **`Client::from_streams`** — connect to a CLI server over arbitrary
  caller-supplied `AsyncRead` / `AsyncWrite`. Useful for testing,
  in-process embedding, or custom transports. Other SDKs are spawn-only
  or fixed-stdio.
- **`enum Transport { Stdio, Tcp, External }`** — explicit, exhaustive
  transport selector on `ClientOptions::transport`. Node/Python/Go rely
  on conditional config field combinations instead.
- **Split `prefix_args` / `extra_args`** on `ClientOptions` — separate
  arg vectors for "prepend before subcommand" vs "append after the
  built-in flags", giving precise control over CLI invocation order
  without string-splicing.

## Layout

| File              | Description                                                                                                                |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `lib.rs`          | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error`                                                              |
| `session.rs`      | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session`                            |
| `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) |
| `handler.rs`      | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler`           |
| `hooks.rs`        | `SessionHooks` trait, `HookEvent`/`HookOutput` enums, typed hook inputs/outputs                                            |
| `transforms.rs`   | `SystemMessageTransform` trait, section-level system message customization                                                 |
| `tool.rs`         | `ToolHandler` trait, `define_tool`, `schema_for::<T>()` (with `derive` feature)                                            |
| `types.rs`        | CLI protocol types (`SessionId`, `SessionEvent`, `SessionConfig`, `Tool`, etc.)                                            |
| `resolve.rs`      | Bundled-CLI resolution (`copilot_binary`)                                                                                  |
| `embeddedcli.rs`  | Embedded CLI extraction (gated on the default `bundled-cli` feature)                                                       |
| `router.rs`       | Internal per-session event demux                                                                                           |
| `jsonrpc.rs`      | Internal Content-Length framed JSON-RPC transport                                                                          |

## Embedded CLI

The SDK bundles the Copilot CLI binary inside the consumer's compiled crate by default. No env var setup, no separate install — just `cargo build` and you get a self-contained binary.

To opt out (e.g. for binary-size-sensitive consumers, or environments that provide the CLI via PATH), set `default-features = false`:

```toml
github-copilot-sdk = { version = "0.1", default-features = false }
```

### How it works

1. **Pinned at publish time.** When the rust crate is published, a workflow step writes `bundled_cli_version.txt` (CLI version + per-platform SHA-256 hashes) into the crate from the in-effect `nodejs/package-lock.json` and the matching GitHub Release's `SHA256SUMS.txt`. This file is gitignored locally; it only exists in the published crate tarball.

2. **Build time:** The SDK's `build.rs` resolves the version + per-platform SHA-256:
   - `COPILOT_CLI_VERSION` env var (advanced override; fetches live `SHA256SUMS.txt`).
   - Otherwise, `bundled_cli_version.txt` from the published crate.
   - Otherwise (mono-repo contributor build), live read from `../nodejs/package-lock.json` + live fetch of `SHA256SUMS.txt`.

   It then downloads the platform-appropriate archive from the [`github/copilot-cli` GitHub Releases]https://github.com/github/copilot-cli/releases (`copilot-{platform}.tar.gz` on macOS/Linux, `.zip` on Windows), verifies the SHA-256, extracts the `copilot` binary, compresses it with zstd, and embeds via `include_bytes!()`.

3. **Runtime:** On the first call to `github_copilot_sdk::Client::start()`, the embedded archive is lazily extracted to the platform cache dir (`%LOCALAPPDATA%\github-copilot-sdk-{version}\` on Windows, `~/Library/Caches/github-copilot-sdk-{version}/` on macOS, `$XDG_CACHE_HOME/github-copilot-sdk-{version}/` (or `~/.cache/...`) on Linux). Subsequent runs reuse the extracted binary.

### Overriding the extraction location

Use [`ClientOptions::with_bundled_cli_extract_dir`] when you need to place the extracted binary somewhere other than the platform cache dir (CI runners with ephemeral homes, sandboxes that disallow cache paths, etc.):

```rust,ignore
use std::path::PathBuf;
use github_copilot_sdk::{Client, ClientOptions};

let options = ClientOptions::new()
    .with_bundled_cli_extract_dir(PathBuf::from("/var/run/my-app/copilot"));
let client = Client::start(options).await?;
```

### Resolution priority

`copilot_binary()` checks these sources in order:

1. Explicit `CliProgram::Path(path)` on `ClientOptions::program`
2. `COPILOT_CLI_PATH` environment variable
3. Embedded CLI (when the `bundled-cli` feature is enabled, which it is by default)

There is no PATH scanning. If both 1+2 are unset and the SDK was built with `default-features = false`, `Client::start` returns `Error::BinaryNotFound`.

### Platforms

Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`. The target platform is auto-detected from `CARGO_CFG_TARGET_OS` and `CARGO_CFG_TARGET_ARCH` (cross-compilation works).

## Features

| Feature        | Default | Description                                                                                                                                               |
| -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bundled-cli`  || Build-time CLI embedding. Pulls in `dirs`, `tar`+`flate2` (Linux/macOS), or `zip` (Windows). Disable via `default-features = false` to opt out (e.g. when shipping a smaller binary or when always supplying the CLI via `CliProgram::Path` / `COPILOT_CLI_PATH`). |
| `derive`       || `schema_for::<T>()` for generating JSON Schema from Rust types (adds `schemars`). Enable when defining [tool parameters]#tool-registration.             |

```toml
# These examples use registry syntax for illustration; until the crate is
# published, use a path or git dependency instead.

# Default — bundles the Copilot CLI in your binary.
github-copilot-sdk = "0.1"

# Opt out of bundling — resolve CLI from COPILOT_CLI_PATH or system PATH instead.
github-copilot-sdk = { version = "0.1", default-features = false }

# Derive JSON Schema for tool parameters (adds to default bundled-cli).
github-copilot-sdk = { version = "0.1", features = ["derive"] }
```