Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 74 additions & 4 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,85 @@ The SDK can only access features exposed through the CLI's JSON-RPC protocol. If

## Version Compatibility

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

The SDK and CLI must have compatible protocol versions. The SDK will log warnings if versions are mismatched.
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.

You can check versions at runtime:

```typescript
const status = await client.ping();
console.log("Server protocol version:", status.protocolVersion);
```

## Protocol v3: Broadcast Events and Multi-Client Sessions

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.

### What Changed

| Aspect | Protocol v2 | Protocol v3 |
|--------|-------------|-------------|
| **Tool calls** | CLI sends RPC request directly to the SDK | CLI broadcasts `external_tool.requested` event to all connected clients |
| **Permission requests** | CLI sends RPC request directly to the SDK | CLI broadcasts `permission.requested` event to all connected clients |
| **Multi-client** | One SDK client per CLI server | Multiple SDK clients can share a CLI server and session |

### How It Works

In v3, the CLI broadcasts tool and permission events to every connected client. Each client checks whether it has a matching handler:

- If the client has a handler for the requested tool, it executes the tool and sends the result back via `session.tools.handlePendingToolCall`.
- If the client doesn't have the handler, it responds with an "unsupported" result.
- Permission requests follow the same pattern via `session.permissions.handlePendingPermissionRequest`.

The SDK handles all of this automatically — you register tools and permission handlers the same way as before:

```typescript
import { CopilotClient, defineTool } from "@github/copilot-sdk";

const myTool = defineTool("my_tool", {
description: "A custom tool",
parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] },
handler: async (args: { input: string }) => {
return { result: args.input.toUpperCase() };
},
});

// Works identically on both v2 and v3
const session = await client.createSession({
tools: [myTool],
onPermissionRequest: approveAll,
});
```

### Multi-Client Sessions

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.

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.

## Upgrading from v2 to v3

Upgrading is straightforward — no code changes required:

1. **Update the SDK package** to the latest version
2. **Update the CLI** to a version that supports protocol v3
3. **That's it** — the SDK auto-negotiates the protocol version

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.

| Step | TypeScript | Python | Go | .NET |
|------|-----------|--------|-----|------|
| 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 |
| Update CLI | `npm install @github/copilot@latest` | Bundled with SDK | External install | Bundled with SDK |

## See Also

- [Getting Started Guide](./getting-started.md)
- [Session Persistence & Multi-Client](./guides/session-persistence.md)
- [Hooks Documentation](./hooks/overview.md)
- [MCP Servers Guide](./mcp/overview.md)
- [Debugging Guide](./debugging.md)
208 changes: 205 additions & 3 deletions docs/guides/session-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,12 +502,13 @@ const session = await client.createSession({
|------------|-------------|------------|
| **BYOK re-authentication** | API keys aren't persisted | Store keys in your secret manager; provide on resume |
| **Writable storage** | `~/.copilot/session-state/` must be writable | Mount persistent volume in containers |
| **No session locking** | Concurrent access to same session is undefined | Implement application-level locking or queue |
| **Tool state not persisted** | In-memory tool state is lost | Design tools to be stateless or persist their own state |

### Handling Concurrent Access

The SDK doesn't provide built-in session locking. If multiple clients might access the same session:
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.

If your use case requires **strict serialization** (e.g., only one client sends prompts at a time), you can still use application-level locking:

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

For multi-client session sharing without locking, see the next section.

## Multi-Client Session Sharing

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.

```mermaid
flowchart TB
subgraph clients["SDK Clients"]
A["Client A<br/>(tool: search_docs)"]
B["Client B<br/>(tool: run_tests)"]
end

subgraph server["CLI Server"]
CLI["Copilot CLI<br/>cliUrl: localhost:3000"]
S["Session: task-123"]
end

A -->|cliUrl| CLI
B -->|cliUrl| CLI
CLI --> S
S -->|broadcast: external_tool.requested| A
S -->|broadcast: external_tool.requested| B
```

### How It Works

1. Start a CLI server (or use an existing one accessible via `cliUrl`)
2. Client A connects and creates a session, registering its tools
3. Client B connects and resumes the same session, registering different tools
4. When the model calls a tool, the CLI broadcasts the request to all clients
5. Each client checks if it has the requested tool and responds accordingly

### TypeScript

```typescript
import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk";

// Client A: provides search capabilities
const searchDocs = defineTool("search_docs", {
description: "Search the documentation",
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
handler: async (args: { query: string }) => {
return { results: [`Result for: ${args.query}`] };
},
});

const clientA = new CopilotClient({ cliUrl: "localhost:3000" });
const sessionA = await clientA.createSession({
sessionId: "shared-task-123",
tools: [searchDocs],
onPermissionRequest: approveAll,
});

// Client B: provides test capabilities (different process or service)
const runTests = defineTool("run_tests", {
description: "Run the test suite",
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
handler: async (args: { path: string }) => {
return { passed: true, path: args.path };
},
});

const clientB = new CopilotClient({ cliUrl: "localhost:3000" });
const sessionB = await clientB.resumeSession("shared-task-123", {
tools: [runTests],
onPermissionRequest: approveAll,
});
```

### Python

```python
from copilot import CopilotClient, PermissionHandler
from copilot.tools import define_tool
from pydantic import BaseModel, Field

# Client A: provides search capabilities
class SearchParams(BaseModel):
query: str = Field(description="Search query")

@define_tool(description="Search the documentation")
async def search_docs(params: SearchParams) -> dict:
return {"results": [f"Result for: {params.query}"]}

client_a = CopilotClient(cli_url="localhost:3000")
await client_a.start()
session_a = await client_a.create_session({
"session_id": "shared-task-123",
"tools": [search_docs],
"on_permission_request": PermissionHandler.approve_all,
})

# Client B: provides test capabilities (different process)
class TestParams(BaseModel):
path: str = Field(description="Test path")

@define_tool(description="Run the test suite")
async def run_tests(params: TestParams) -> dict:
return {"passed": True, "path": params.path}

client_b = CopilotClient(cli_url="localhost:3000")
await client_b.start()
session_b = await client_b.resume_session("shared-task-123", {
"tools": [run_tests],
"on_permission_request": PermissionHandler.approve_all,
})
```

### Go

<!-- docs-validate: skip -->
```go
ctx := context.Background()

// Client A: provides search capabilities
searchDocs := copilot.DefineTool(
"search_docs",
"Search the documentation",
func(params struct {
Query string `json:"query" jsonschema:"Search query"`
}, inv copilot.ToolInvocation) (map[string]any, error) {
return map[string]any{"results": []string{fmt.Sprintf("Result for: %s", params.Query)}}, nil
},
)

clientA := copilot.NewClient(&copilot.ClientOptions{CLIUrl: "localhost:3000"})
sessionA, _ := clientA.CreateSession(ctx, &copilot.SessionConfig{
SessionID: "shared-task-123",
Tools: []copilot.Tool{searchDocs},
OnPermissionRequest: copilot.ApproveAll,
})

// Client B: provides test capabilities (different process)
runTests := copilot.DefineTool(
"run_tests",
"Run the test suite",
func(params struct {
Path string `json:"path" jsonschema:"Test path"`
}, inv copilot.ToolInvocation) (map[string]any, error) {
return map[string]any{"passed": true, "path": params.Path}, nil
},
)

clientB := copilot.NewClient(&copilot.ClientOptions{CLIUrl: "localhost:3000"})
sessionB, _ := clientB.ResumeSession(ctx, "shared-task-123", &copilot.ResumeSessionConfig{
Tools: []copilot.Tool{runTests},
OnPermissionRequest: copilot.ApproveAll,
})
```

### C# (.NET)

<!-- docs-validate: skip -->
```csharp
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
using System.ComponentModel;

// Client A: provides search capabilities
var searchDocs = AIFunctionFactory.Create(
([Description("Search query")] string query) =>
new { results = new[] { $"Result for: {query}" } },
"search_docs",
"Search the documentation"
);

var clientA = new CopilotClient(new CopilotClientOptions { CliUrl = "localhost:3000" });
var sessionA = await clientA.CreateSessionAsync(new SessionConfig
{
SessionId = "shared-task-123",
Tools = [searchDocs],
OnPermissionRequest = PermissionHandler.ApproveAll,
});

// Client B: provides test capabilities (different process)
var runTests = AIFunctionFactory.Create(
([Description("Test path")] string path) =>
new { passed = true, path },
"run_tests",
"Run the test suite"
);

var clientB = new CopilotClient(new CopilotClientOptions { CliUrl = "localhost:3000" });
var sessionB = await clientB.ResumeSessionAsync("shared-task-123", new ResumeSessionConfig
{
Tools = [runTests],
OnPermissionRequest = PermissionHandler.ApproveAll,
});
```

### Best Practices for Multi-Client Sessions

| Practice | Description |
|----------|-------------|
| **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. |
| **Always provide `onPermissionRequest`** | Each client that might receive permission broadcasts should have a handler. |
| **Use meaningful session IDs** | Shared sessions need predictable IDs so all clients can find them. |
| **Handle disconnections gracefully** | If a client disconnects, its tools become unavailable. Design your system so remaining clients can still operate. |

## Summary

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

## Next Steps

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