Skip to content

Commit 474de8b

Browse files
committed
Docs
1 parent 236ac94 commit 474de8b

2 files changed

Lines changed: 279 additions & 7 deletions

File tree

docs/compatibility.md

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,85 @@ The SDK can only access features exposed through the CLI's JSON-RPC protocol. If
174174

175175
## Version Compatibility
176176

177-
| SDK Version | CLI Version | Protocol Version |
178-
|-------------|-------------|------------------|
179-
| Check `package.json` | `copilot --version` | `getStatus().protocolVersion` |
177+
| SDK Version | Min Protocol | Max Protocol | Notes |
178+
|-------------|-------------|--------------|-------|
179+
| 0.1.x | 2 | 2 | Exact match required |
180+
| 0.2.x+ | 2 | 3 | Range-based negotiation |
180181

181-
The SDK and CLI must have compatible protocol versions. The SDK will log warnings if versions are mismatched.
182+
The SDK and CLI negotiate a compatible protocol version at startup. The SDK advertises a supported range (`minVersion``version`) and the CLI reports its version via `ping`. If the CLI's version falls within the SDK's range, the connection succeeds; otherwise, the SDK throws an error.
183+
184+
You can check versions at runtime:
185+
186+
```typescript
187+
const status = await client.ping();
188+
console.log("Server protocol version:", status.protocolVersion);
189+
```
190+
191+
## Protocol v3: Broadcast Events and Multi-Client Sessions
192+
193+
Protocol v3 changes how the SDK handles tool calls and permission requests internally. **No user-facing API changes are required** — existing code continues to work.
194+
195+
### What Changed
196+
197+
| Aspect | Protocol v2 | Protocol v3 |
198+
|--------|-------------|-------------|
199+
| **Tool calls** | CLI sends RPC request directly to the SDK | CLI broadcasts `external_tool.requested` event to all connected clients |
200+
| **Permission requests** | CLI sends RPC request directly to the SDK | CLI broadcasts `permission.requested` event to all connected clients |
201+
| **Multi-client** | One SDK client per CLI server | Multiple SDK clients can share a CLI server and session |
202+
203+
### How It Works
204+
205+
In v3, the CLI broadcasts tool and permission events to every connected client. Each client checks whether it has a matching handler:
206+
207+
- If the client has a handler for the requested tool, it executes the tool and sends the result back via `session.tools.handlePendingToolCall`.
208+
- If the client doesn't have the handler, it responds with an "unsupported" result.
209+
- Permission requests follow the same pattern via `session.permissions.handlePendingPermissionRequest`.
210+
211+
The SDK handles all of this automatically — you register tools and permission handlers the same way as before:
212+
213+
```typescript
214+
import { CopilotClient, defineTool } from "@github/copilot-sdk";
215+
216+
const myTool = defineTool("my_tool", {
217+
description: "A custom tool",
218+
parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] },
219+
handler: async (args: { input: string }) => {
220+
return { result: args.input.toUpperCase() };
221+
},
222+
});
223+
224+
// Works identically on both v2 and v3
225+
const session = await client.createSession({
226+
tools: [myTool],
227+
onPermissionRequest: approveAll,
228+
});
229+
```
230+
231+
### Multi-Client Sessions
232+
233+
With v3, multiple SDK clients can connect to the same CLI server (via `cliUrl`) and share sessions. Each client can register different tools, and the broadcast model routes tool calls to the client that has the matching handler.
234+
235+
See the [Multi-Client Session Sharing](./guides/session-persistence.md#multi-client-session-sharing) section in the Session Persistence guide for details and code samples.
236+
237+
## Upgrading from v2 to v3
238+
239+
Upgrading is straightforward — no code changes required:
240+
241+
1. **Update the SDK package** to the latest version
242+
2. **Update the CLI** to a version that supports protocol v3
243+
3. **That's it** — the SDK auto-negotiates the protocol version
244+
245+
The SDK remains backward-compatible with v2 CLI servers. If the CLI only supports v2, the SDK operates in v2 mode automatically. Multi-client session features are only available when both the SDK and CLI use v3.
246+
247+
| Step | TypeScript | Python | Go | .NET |
248+
|------|-----------|--------|-----|------|
249+
| Update SDK | `npm install @github/copilot-sdk@latest` | `pip install --upgrade copilot-sdk` | `go get github.com/github/copilot-sdk/go@latest` | Update `PackageReference` version |
250+
| Update CLI | `npm install @github/copilot@latest` | Bundled with SDK | External install | Bundled with SDK |
182251

183252
## See Also
184253

185254
- [Getting Started Guide](./getting-started.md)
255+
- [Session Persistence & Multi-Client](./guides/session-persistence.md)
186256
- [Hooks Documentation](./hooks/overview.md)
187257
- [MCP Servers Guide](./mcp/overview.md)
188258
- [Debugging Guide](./debugging.md)

docs/guides/session-persistence.md

Lines changed: 205 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -502,12 +502,13 @@ const session = await client.createSession({
502502
|------------|-------------|------------|
503503
| **BYOK re-authentication** | API keys aren't persisted | Store keys in your secret manager; provide on resume |
504504
| **Writable storage** | `~/.copilot/session-state/` must be writable | Mount persistent volume in containers |
505-
| **No session locking** | Concurrent access to same session is undefined | Implement application-level locking or queue |
506505
| **Tool state not persisted** | In-memory tool state is lost | Design tools to be stateless or persist their own state |
507506

508507
### Handling Concurrent Access
509508

510-
The SDK doesn't provide built-in session locking. If multiple clients might access the same session:
509+
With protocol v3, the SDK supports **multi-client session sharing** — multiple SDK clients can connect to the same CLI server and operate on the same session simultaneously. The CLI broadcasts tool and permission events to all connected clients, and each client handles the events for tools it has registered.
510+
511+
If your use case requires **strict serialization** (e.g., only one client sends prompts at a time), you can still use application-level locking:
511512

512513
```typescript
513514
// Option 1: Application-level locking with Redis
@@ -540,12 +541,213 @@ await withSessionLock("user-123-task-456", async () => {
540541
});
541542
```
542543

544+
For multi-client session sharing without locking, see the next section.
545+
546+
## Multi-Client Session Sharing
547+
548+
Protocol v3 enables multiple SDK clients to share a session via broadcast events. This is useful for architectures where different services each provide different tools, or where a session needs to be accessible from multiple processes.
549+
550+
```mermaid
551+
flowchart TB
552+
subgraph clients["SDK Clients"]
553+
A["Client A<br/>(tool: search_docs)"]
554+
B["Client B<br/>(tool: run_tests)"]
555+
end
556+
557+
subgraph server["CLI Server"]
558+
CLI["Copilot CLI<br/>cliUrl: localhost:3000"]
559+
S["Session: task-123"]
560+
end
561+
562+
A -->|cliUrl| CLI
563+
B -->|cliUrl| CLI
564+
CLI --> S
565+
S -->|broadcast: external_tool.requested| A
566+
S -->|broadcast: external_tool.requested| B
567+
```
568+
569+
### How It Works
570+
571+
1. Start a CLI server (or use an existing one accessible via `cliUrl`)
572+
2. Client A connects and creates a session, registering its tools
573+
3. Client B connects and resumes the same session, registering different tools
574+
4. When the model calls a tool, the CLI broadcasts the request to all clients
575+
5. Each client checks if it has the requested tool and responds accordingly
576+
577+
### TypeScript
578+
579+
```typescript
580+
import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk";
581+
582+
// Client A: provides search capabilities
583+
const searchDocs = defineTool("search_docs", {
584+
description: "Search the documentation",
585+
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
586+
handler: async (args: { query: string }) => {
587+
return { results: [`Result for: ${args.query}`] };
588+
},
589+
});
590+
591+
const clientA = new CopilotClient({ cliUrl: "localhost:3000" });
592+
const sessionA = await clientA.createSession({
593+
sessionId: "shared-task-123",
594+
tools: [searchDocs],
595+
onPermissionRequest: approveAll,
596+
});
597+
598+
// Client B: provides test capabilities (different process or service)
599+
const runTests = defineTool("run_tests", {
600+
description: "Run the test suite",
601+
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
602+
handler: async (args: { path: string }) => {
603+
return { passed: true, path: args.path };
604+
},
605+
});
606+
607+
const clientB = new CopilotClient({ cliUrl: "localhost:3000" });
608+
const sessionB = await clientB.resumeSession("shared-task-123", {
609+
tools: [runTests],
610+
onPermissionRequest: approveAll,
611+
});
612+
```
613+
614+
### Python
615+
616+
```python
617+
from copilot import CopilotClient, PermissionHandler
618+
from copilot.tools import define_tool
619+
from pydantic import BaseModel, Field
620+
621+
# Client A: provides search capabilities
622+
class SearchParams(BaseModel):
623+
query: str = Field(description="Search query")
624+
625+
@define_tool(description="Search the documentation")
626+
async def search_docs(params: SearchParams) -> dict:
627+
return {"results": [f"Result for: {params.query}"]}
628+
629+
client_a = CopilotClient(cli_url="localhost:3000")
630+
await client_a.start()
631+
session_a = await client_a.create_session({
632+
"session_id": "shared-task-123",
633+
"tools": [search_docs],
634+
"on_permission_request": PermissionHandler.approve_all,
635+
})
636+
637+
# Client B: provides test capabilities (different process)
638+
class TestParams(BaseModel):
639+
path: str = Field(description="Test path")
640+
641+
@define_tool(description="Run the test suite")
642+
async def run_tests(params: TestParams) -> dict:
643+
return {"passed": True, "path": params.path}
644+
645+
client_b = CopilotClient(cli_url="localhost:3000")
646+
await client_b.start()
647+
session_b = await client_b.resume_session("shared-task-123", {
648+
"tools": [run_tests],
649+
"on_permission_request": PermissionHandler.approve_all,
650+
})
651+
```
652+
653+
### Go
654+
655+
<!-- docs-validate: skip -->
656+
```go
657+
ctx := context.Background()
658+
659+
// Client A: provides search capabilities
660+
searchDocs := copilot.DefineTool(
661+
"search_docs",
662+
"Search the documentation",
663+
func(params struct {
664+
Query string `json:"query" jsonschema:"Search query"`
665+
}, inv copilot.ToolInvocation) (map[string]any, error) {
666+
return map[string]any{"results": []string{fmt.Sprintf("Result for: %s", params.Query)}}, nil
667+
},
668+
)
669+
670+
clientA := copilot.NewClient(&copilot.ClientOptions{CLIUrl: "localhost:3000"})
671+
sessionA, _ := clientA.CreateSession(ctx, &copilot.SessionConfig{
672+
SessionID: "shared-task-123",
673+
Tools: []copilot.Tool{searchDocs},
674+
OnPermissionRequest: copilot.ApproveAll,
675+
})
676+
677+
// Client B: provides test capabilities (different process)
678+
runTests := copilot.DefineTool(
679+
"run_tests",
680+
"Run the test suite",
681+
func(params struct {
682+
Path string `json:"path" jsonschema:"Test path"`
683+
}, inv copilot.ToolInvocation) (map[string]any, error) {
684+
return map[string]any{"passed": true, "path": params.Path}, nil
685+
},
686+
)
687+
688+
clientB := copilot.NewClient(&copilot.ClientOptions{CLIUrl: "localhost:3000"})
689+
sessionB, _ := clientB.ResumeSession(ctx, "shared-task-123", &copilot.ResumeSessionConfig{
690+
Tools: []copilot.Tool{runTests},
691+
OnPermissionRequest: copilot.ApproveAll,
692+
})
693+
```
694+
695+
### C# (.NET)
696+
697+
<!-- docs-validate: skip -->
698+
```csharp
699+
using GitHub.Copilot.SDK;
700+
using Microsoft.Extensions.AI;
701+
using System.ComponentModel;
702+
703+
// Client A: provides search capabilities
704+
var searchDocs = AIFunctionFactory.Create(
705+
([Description("Search query")] string query) =>
706+
new { results = new[] { $"Result for: {query}" } },
707+
"search_docs",
708+
"Search the documentation"
709+
);
710+
711+
var clientA = new CopilotClient(new CopilotClientOptions { CliUrl = "localhost:3000" });
712+
var sessionA = await clientA.CreateSessionAsync(new SessionConfig
713+
{
714+
SessionId = "shared-task-123",
715+
Tools = [searchDocs],
716+
OnPermissionRequest = PermissionHandler.ApproveAll,
717+
});
718+
719+
// Client B: provides test capabilities (different process)
720+
var runTests = AIFunctionFactory.Create(
721+
([Description("Test path")] string path) =>
722+
new { passed = true, path },
723+
"run_tests",
724+
"Run the test suite"
725+
);
726+
727+
var clientB = new CopilotClient(new CopilotClientOptions { CliUrl = "localhost:3000" });
728+
var sessionB = await clientB.ResumeSessionAsync("shared-task-123", new ResumeSessionConfig
729+
{
730+
Tools = [runTests],
731+
OnPermissionRequest = PermissionHandler.ApproveAll,
732+
});
733+
```
734+
735+
### Best Practices for Multi-Client Sessions
736+
737+
| Practice | Description |
738+
|----------|-------------|
739+
| **Distribute tools uniquely** | Each tool should be registered on exactly one client. If multiple clients register the same tool, only one response will be used. |
740+
| **Always provide `onPermissionRequest`** | Each client that might receive permission broadcasts should have a handler. |
741+
| **Use meaningful session IDs** | Shared sessions need predictable IDs so all clients can find them. |
742+
| **Handle disconnections gracefully** | If a client disconnects, its tools become unavailable. Design your system so remaining clients can still operate. |
743+
543744
## Summary
544745

545746
| Feature | How to Use |
546747
|---------|------------|
547748
| **Create resumable session** | Provide your own `sessionId` |
548749
| **Resume session** | `client.resumeSession(sessionId)` |
750+
| **Multi-client sharing** | Multiple clients connect via `cliUrl`, each registers its own tools |
549751
| **BYOK resume** | Re-provide `provider` config |
550752
| **List sessions** | `client.listSessions(filter?)` |
551753
| **Disconnect from active session** | `session.disconnect()` — releases in-memory resources; session data on disk is preserved for resumption |
@@ -554,6 +756,6 @@ await withSessionLock("user-123-task-456", async () => {
554756

555757
## Next Steps
556758

759+
- [Compatibility Guide](../compatibility.md) - SDK vs CLI feature comparison, protocol v3 details
557760
- [Hooks Overview](../hooks/overview.md) - Customize session behavior with hooks
558-
- [Compatibility Guide](../compatibility.md) - SDK vs CLI feature comparison
559761
- [Debugging Guide](../debugging.md) - Troubleshoot session issues

0 commit comments

Comments
 (0)