GitHub Copilot SDK

Programmable agent runtime, four languages, one wire contract

Embed the Copilot CLI's planning, tool-use, and file-editing engine directly into your application via JSON-RPC. Ships as parallel SDKs for Node.js, Python, Go, and .NET — each native-feeling, all driven by the same schema-generated protocol.

v0.3.0-preview.0 Protocol v3 Public Preview Multi-language

4
SDK languages
3
Protocol version
40+
Event types
~25k
Lines of SDK code
338
Source files
YAML
Conformance suite

🎯 Project Philosophy

When you first open this repository, the most striking design choice is the absence of a shared runtime. There is no "core library" that all SDKs thunk into; no shared C++ module, no WebAssembly shim, no RPC proxy written in Rust. Instead, four SDKs reimplement the same behaviour from scratch — in TypeScript, Python, Go, and C# — and guarantee parity through three enforcement layers: a shared JSON Schema for the wire contract, a code generator that emits the RPC boilerplate, and a YAML-driven conformance test suite that diffs behaviour across all four.

This is a deliberate trade. A shared core would have been faster to build initially. But anyone who has used polyglot SDKs derived from a single code base knows the feeling — the Node SDK is a joy; the Python one is a thin wrapper over unmarshalled JSON; the Go one has a quirky camelCase API. By committing to per-language idiomaticity, every user of every SDK gets a first-class experience: Python users get async with CopilotClient(), .NET users get await using + CancellationToken, Go users get DefineTool[T,U] generics, TS users get discriminated-union SessionEvents. The cost — four parallel implementations — is managed by the contract-enforcement triad.

The second architectural commitment is treating the Copilot CLI binary as the server. Not an HTTP service, not a library. A sub-process. This feels old-school until you recognise how many problems it eliminates: process death is an OS-level signal (not a TCP timeout), clean-up is SIGKILL, authentication is inherited from the shell session, updates are a single binary swap, and no language's SDK has to reimplement the agent loop. The JSON-RPC framing (LSP-style Content-Length headers) is boring and well-understood; every SDK's "hardest file" is still under 400 lines.

The SDK prioritises correctness over performance and explicit over implicit. Every async entry point takes a cancellation token (or language equivalent). Capabilities are negotiated at session start — no try-call-catch-MethodNotFound patterns. Permission handlers get full tool call context (not just a boolean gate). The tool-use loop is observable via 40+ typed events, not a callback into a black box.

🏗️ Design Decisions

DecisionChoice MadeAlternativesWhy This Approach
Polyglot architectureFour independent SDKs, no shared runtimeShared core via FFI; WASM; gRPC proxyEvery language gets idiomatic API; no transitive dependency chain; contract-driven parity rather than code-driven
TransportJSON-RPC 2.0 over stdio or TCP, Content-Length framedgRPC, HTTP, WebSocket, custom binaryWell-understood (LSP uses it), no extra deps per SDK, stdio gives OS-level back-pressure and lifecycle signals for free
Server topologyCLI spawned as sub-processHTTP daemon; embedded library; system serviceOS process semantics handle death/cleanup; auth inherited; no service management surface
Contract enforcementJSON Schema → codegen → conformance testsHand-maintained docs; integration tests onlyThree layers mean no silent drift: schema catches at compile time, tests catch at runtime
Protocol versioningSingle integer in sdk-protocol-version.jsonSemver on wire; per-method versions; feature bitmapSmall matrix of compatible combinations; capability flags per session handle sub-version features
ExtensibilityUser-defined tools, commands, hooks, elicitation callbacksPlugin loader; script embeddingLanguage-native callbacks keep the extension surface tiny; hooks are pure function interposition
AuthInherited from CLI login OR env vars OR BYOK provider keysSDK-managed OAuth flowThe CLI already solves auth — one place to log in, one place to rotate tokens
Session FSOptional SessionFsProvider interfaceAlways write to real diskLets users run sandboxed or in-memory session workspaces; test harnesses use it to avoid disk side-effects

System Architecture

flowchart TD
  subgraph User["Your Application"]
    App[App code]
  end
  subgraph SDK_LANG["SDK layer - pick one"]
    NODE[nodejs SDK
client.ts, session.ts] PY[python SDK
client.py, session.py] GO[go SDK
client.go, session.go] NET[dotnet SDK
Client.cs, Session.cs] end subgraph CONTRACT["Shared contract"] SCH[api.schema.json
session-events.schema.json] VER[sdk-protocol-version.json] CODEGEN[scripts codegen
emits rpc + session_events] TESTS[test snapshots YAML
conformance fixtures] end subgraph CLI["Copilot CLI subprocess"] AGT[Agent loop
plan -> tool -> respond] TOOLS[Builtin tools
shell, file, web] MCP[MCP servers
optional sub-processes] end App -->|language-native API| NODE App -->|language-native API| PY App -->|language-native API| GO App -->|language-native API| NET NODE -->|JSON-RPC / stdio or TCP| AGT PY -->|JSON-RPC / stdio or TCP| AGT GO -->|JSON-RPC / stdio or TCP| AGT NET -->|JSON-RPC / stdio or TCP| AGT SCH -->|read at build| CODEGEN VER -->|embedded constant| NODE VER -->|embedded constant| PY VER -->|embedded constant| GO VER -->|embedded constant| NET CODEGEN -->|generated/ rpc.ts| NODE CODEGEN -->|generated/ rpc.py| PY CODEGEN -->|rpc generated_rpc.go| GO CODEGEN -->|Generated/ Rpc.cs| NET TESTS -.runs against.-> NODE TESTS -.runs against.-> PY TESTS -.runs against.-> GO TESTS -.runs against.-> NET AGT --> TOOLS AGT --> MCP

Modules

Language SDKs

nodejs

TypeScript / Node.js SDK — reference implementation

Ships as @github/copilot-sdk on npm. Uses vscode-jsonrpc for transport; supports Zod or plain JSON Schema for tool params. Bundles the Copilot CLI as a transitive npm dependency.

98 .ts files~5k LOCESM only

python

Async Python SDK — asyncio-native, 3.11+

Ships as github-copilot-sdk on PyPI. Threaded JSON-RPC bridge sidesteps asyncio subprocess pitfalls on Windows. async with lifecycle; TypedDict payloads; stdlib-only.

79 .py files~6.5k LOCasyncio

go

Go SDK — idiomatic generics, cross-platform file-lock

Uses Go 1.18+ generics for DefineTool[T,U]. Custom internal/jsonrpc2 transport; flock-guarded CLI extraction; build-tag split files for Windows/Unix syscalls.

85 .go files~4.6k LOCGo 1.21+

dotnet

.NET 8.0+ SDK — System.Text.Json + nullable refs

NuGet package GitHub.Copilot.SDK. Source-generated converters for polymorphic events; IAsyncDisposable lifecycle; Channel<T> for event streaming; custom MillisecondsTimeSpanConverter.

76 .cs files~4.3k LOC.NET 8+

Shared infrastructure

scripts

Codegen — the contract layer

Reads JSON Schemas shipped with the CLI and emits RPC bindings into each SDK's generated/ folder. Four language-specific emitters share utils.ts for schema normalisation.

TypeScriptquicktypejson-schema-to-typescript

test

Cross-SDK conformance suite

YAML scenarios run through every SDK and diff against stored snapshots. Organised by feature (permissions/, hooks_extended/, session_fs/, ...), not by SDK — enforces behavioural parity.

YAML fixturesSnapshot diffPer-feature folders

🔗 Key Data Flows

Flow A — Single-prompt conversation

sequenceDiagram
    participant App as Your App
    participant SDK as SDK Client
    participant CLI as Copilot CLI subprocess
    participant LLM as LLM provider

    App->>SDK: new CopilotClient / createSession
    SDK->>CLI: spawn / TCP connect
    SDK->>CLI: JSON-RPC getStatus
    CLI-->>SDK: protocolVersion + capabilities
    Note over SDK,CLI: WHY: negotiate features before first turn
    SDK->>CLI: JSON-RPC session.create
    CLI-->>SDK: sessionId + session.capabilities
    App->>SDK: session.sendAndWait(prompt)
    SDK->>SDK: register idle listener (race fix)
    SDK->>CLI: JSON-RPC session.send
    CLI->>LLM: prompt + tools + system message
    LLM-->>CLI: assistant response
    CLI-->>SDK: event: assistant.message (streamed)
    CLI-->>SDK: event: session.idle
    SDK-->>App: resolved Promise / Task / channel
    App->>SDK: client.stop() / dispose
    SDK->>CLI: JSON-RPC shutdown

Flow B — Tool invocation round-trip

sequenceDiagram
    participant LLM as LLM
    participant CLI as Copilot CLI
    participant SDK as SDK (server-to-client RPC)
    participant Hook as preToolUse hook
    participant Perm as Permission handler
    participant Tool as User tool handler

    LLM->>CLI: decide: call get_weather(Seattle)
    CLI->>SDK: session/toolInvoke RPC
    Note over CLI,SDK: WHY: CLI calls back into SDK because tool lives in user code
    SDK->>Hook: pre-tool-use(toolName, args)
    Hook-->>SDK: modified args OR deny
    SDK->>Perm: request approval?
    Note over SDK,Perm: WHY: user gates destructive actions before execution
    Perm-->>SDK: approved
    SDK->>Tool: handler(args)
    Tool-->>SDK: "Cloudy, 62F"
    SDK-->>CLI: RPC reply with result
    CLI->>LLM: tool result appended to context
    LLM-->>CLI: final assistant message
    CLI-->>SDK: event: assistant.message + session.idle

Flow C — Codegen pipeline (build time)

flowchart LR
  SCH[api.schema.json
session-events.schema.json
from @github/copilot/schemas] UTILS[scripts/codegen/utils.ts
postProcessSchema] TSE[typescript.ts] PYE[python.ts] GOE[go.ts] CSE[csharp.ts] NODE[nodejs/src/generated/rpc.ts
nodejs/src/generated/session-events.ts] PY[python/copilot/generated/rpc.py
session_events.py] GO[go/rpc/generated_rpc.go] NET[dotnet/src/Generated/Rpc.cs
SessionEvents.cs] SCH --> UTILS UTILS --> TSE --> NODE UTILS --> PYE --> PY UTILS --> GOE --> GO UTILS --> CSE --> NET
LevelWhat You'll FindWhen to Use
L0 — Project Hub (this page)Philosophy, cross-SDK design, system flowsNew to the repo
L1 — Module OverviewPer-SDK architecture, components, design rationaleUsing or working on a specific SDK
Focus pagesDeep-dive: JSON-RPC framing, protocol negotiation, codegen pipelineUnderstanding cross-cutting mechanisms

How SDK & CLI Connect

Every SDK speaks to the CLI through the same two-way JSON-RPC channel. Understanding the boundary is the single most important concept:

sequenceDiagram
    participant App as User App
    participant SDK as SDK client-side handlers
    participant RPC as JSON-RPC connection
Content-Length framed participant CLI as CLI server-side handlers App->>SDK: client.createSession() SDK->>RPC: request: session.create {config} RPC->>CLI: bytes CLI->>CLI: spawn internal session CLI-->>RPC: response {sessionId, capabilities} Note over RPC: SDK -> CLI: user-intent calls App->>SDK: session.send("hello") SDK->>RPC: request: session.send RPC->>CLI: bytes CLI->>RPC: notification: assistant.message RPC->>SDK: dispatched to SessionEvent listeners SDK-->>App: event callback fires CLI->>RPC: request: session/toolInvoke {tool,args} Note over RPC: CLI -> SDK: tool/permission/FS calls (bidirectional!) RPC->>SDK: dispatched to registered tool handler SDK->>App: user's tool code runs App-->>SDK: tool result SDK->>RPC: response {result} RPC->>CLI: bytes CLI->>RPC: notification: session.idle RPC->>SDK: resolves sendAndWait promise
Key insight. The JSON-RPC channel is bidirectional. The SDK makes calls like session.create and session.send; the CLI calls back for tool execution, permissions, FS operations, and elicitation. Every SDK must register both client-to-server call shims AND server-to-client handler maps on connect. This is why every SDK has a registerClientSessionApiHandlers-equivalent function.

Directory Map

Project source tree
copilot-sdk/
├── nodejs/                       ← TypeScript SDK (reference impl)
│   ├── src/                      ← client.ts, session.ts, types.ts, sessionFsProvider.ts
│   ├── src/generated/            ← codegen output (rpc.ts, session-events.ts)
│   ├── samples/ examples/        ← chat demo, basic-example
│   ├── scripts/                  ← update-protocol-version
│   └── test/e2e/                 ← end-to-end tests
├── python/                       ← Async Python SDK
│   ├── copilot/                  ← client.py, session.py, _jsonrpc.py, tools.py
│   ├── copilot/generated/        ← codegen output
│   ├── samples/ e2e/             ← chat demo + harness
│   └── scripts/
├── go/                           ← Go SDK
│   ├── client.go session.go      ← main entry points
│   ├── definetool.go             ← generic tool wrapper
│   ├── internal/jsonrpc2/        ← custom JSON-RPC impl
│   ├── internal/embeddedcli/     ← bundled CLI extraction
│   ├── internal/flock/           ← cross-platform file locking
│   ├── rpc/                      ← codegen output
│   ├── cmd/bundler/              ← release-time bundling tool
│   └── samples/
├── dotnet/                       ← .NET SDK
│   ├── src/                      ← Client.cs, Session.cs, Types.cs
│   ├── src/Generated/            ← codegen output
│   ├── samples/ test/
├── scripts/                      ← codegen pipeline
│   ├── codegen/                  ← typescript.ts, python.ts, go.ts, csharp.ts, utils.ts
│   ├── corrections/              ← hand-patched regions
│   └── docs-validation/
├── test/                         ← cross-SDK conformance
│   ├── snapshots/                ← YAML fixtures per feature
│   └── harness/ scenarios/
├── docs/                         ← narrative docs (features, hooks, auth)
├── assets/                       ← images for README
├── justfile                      ← dev task runner
├── sdk-protocol-version.json     ← single-source protocol integer
├── CHANGELOG.md                  ← per-release feature log
└── README.md

Build & Tech Stack

Languages

TypeScript, Python, Go, C# + YAML fixtures

Build tools

npm (TS), hatch (Py), go build, dotnet + NuGet, just for cross-repo tasks

Codegen

json-schema-to-typescript, quicktype

Transport

vscode-jsonrpc (TS), hand-rolled elsewhere

Auth

OAuth via CLI, env tokens, BYOK (OpenAI / Anthropic / Azure)

Release cadence

Public preview — see CHANGELOG.md