go — Go SDK

github.com/github/copilot-sdk/go — generics, flock, cross-platform syscalls.

The Go SDK is built for cloud-native integrations: Kubernetes operators, CI runners, backend services, CLI tools. Ships as a single import path; compiles into a static binary with the CLI either externally installed or bundled and extracted on first use. Uses Go 1.18+ generics for type-safe tool handlers (DefineTool[T, U]), a hand-rolled internal/jsonrpc2 transport, and platform-split files for Windows/Unix syscalls.

Go 1.21+ Static binary Generics

85
.go files
1814
client.go
1270
session.go
1213
types.go
4
flock variants

📚 What is this & why does it exist?

A Go SDK exposing copilot.Client and copilot.Session types. You call client := copilot.NewClient(nil), client.Start(), defer client.Stop() — then create sessions, register tool handlers, and process events through channels and callbacks. The SDK handles subprocess spawning, JSON-RPC framing, cross-platform file locking (for bundled-CLI extraction), and typed tool argument unmarshaling via Go generics.

💡 Real-world analogy

Think of the Go SDK as a self-contained embassy kit for a static building. Every embassy (binary) ships with its own printed embassy manual (the bundled CLI), its own lockbox keys that differ per country (flock_windows vs flock_unix), and generic passport forms that get filled in with real types (DefineTool[T,U]) at compile time so you never get a "wrong form" error at the border. Because the embassy is self-contained, you can deploy it anywhere Go runs with scp and a single binary drop.

🎯 Why This Module Exists

Go dominates cloud-native tooling: Kubernetes operators, Terraform providers, CI systems, observability agents. A Go SDK unlocks embedding Copilot's agent loop into these systems without shelling out — preserving the event stream, permission flow, and tool callback ergonomics. Because Go compiles to a single static binary, a Copilot-powered operator can be deployed with kubectl apply and a pulled image, no runtime install required.

The Go SDK also showcases something the other SDKs don't: an embedded CLI pattern. Go can't trivially depend on npm packages, so internal/embeddedcli ships the CLI binary inside the SDK and extracts it on first use to a content-addressable cache directory. Concurrent processes race to extract — handled by platform-specific flock advisory locks.

🏗️ Design Decisions

DecisionChoice MadeAlternativesWhy This Approach
JSON-RPC libraryHand-rolled internal/jsonrpc2sourcegraph/jsonrpc2, creachadair/jrpc2External libs assume HTTP/websocket; we need Content-Length-framed stdio — a different transport
Tool handlersDefineTool[T, U] with generics + reflection on Tinterface{} args; code generationGenerics give compile-time arg typing and let schema generation derive from the same type
Cross-platform syscallsBuild-tag split files: process_{windows,other}.go, flock_{unix,windows,other}.goruntime.GOOS switches in one fileBuild tags isolate platform imports; each file compiles only on its target OS
Concurrency primitivessync.Mutex, sync.RWMutex, atomic.Pointer[os.Process]Single mutex; channels-onlyFine-grained locks match access patterns: RW for start/stop (rare write, many reads); atomic for process-pointer reads during shutdown
CLI bundlinginternal/embeddedcli + flock-guarded extractionRequire CLI install; use go:embedflock handles parallel-process races; extraction keeps binary size normal; go:embed would 3x the compiled size
Public API stylePointer receivers; NewClient constructor; nil options = defaultsExported struct direct initConstructor enforces invariants; lets internal fields evolve without breaking the API
Result unionsHand-written rpc/result_union.go with type tagsBare interface{}; codegen-attempted unionsCodegen can't yet emit idiomatic Go unions; hand-written keeps user code clean: if d, ok := event.Data.(*AssistantMessageData); ok {}

Architecture

flowchart LR
  subgraph pub["public (package copilot)"]
    CL[Client
client.go] SE[Session
session.go] T[DefineTool[T,U]
definetool.go] PH[PermissionHandler
permissions.go] TY[Types
types.go] SF[SessionFsProvider
session_fs_provider.go] end subgraph int["internal (unexported)"] JR[jsonrpc2
custom transport] EC[embeddedcli
extraction] FL[flock
cross-platform] end subgraph gen["rpc (generated + hand-written)"] GR[generated_rpc.go
codegen] RU[result_union.go
hand-written] end CLIPROC[copilot subprocess] CL --> JR CL --> EC EC --> FL SE --> JR CL --> GR SE --> GR CL --> TY SE --> TY SE --> T SE --> PH SE --> SF GR --> RU JR -->|stdio / TCP| CLIPROC

🚶 Narrated Walkthrough

Follow session.Send(copilot.MessageOptions{Prompt: "hi"}) through the SDK:

  1. copilot.NewClient(nil) — returns a zero-value-safe *Client. No I/O.
  2. client.Start() — acquires startStopMux as writer. If CLIUrl is set, dials TCP. Otherwise resolves CLI path: Options.CLIPath → $PATH lookup → internal/embeddedcli extraction.
  3. embeddedcli extraction (bundled path) — compute content hash, check cache. If missing: acquire flock on <target>.lock, re-check, extract to .tmp, chmod 0755, atomic rename, release lock.
  4. Spawn — exec.CommandContext(ctx, cliPath, "serve", "--stdio"); wire stdin/stdout.
  5. jsonrpc2.NewClient — starts reader goroutine that loops: parse Content-Length header, read N bytes, unmarshal, dispatch by kind (response/notification/request).
  6. Handshake — client.RPC.GetStatus(ctx). Cache negotiatedProtocolVersion.
  7. CreateSession(ctx, cfg) — acquires startStopMux reader. Serialises cfg, sends session.create RPC. Registers callbacks into sessions[sessionId] = &Session{...}.
  8. session.On(func(evt SessionEvent){...}) — appends to eventHandlers slice (under mutex).
  9. session.Send(MessageOptions{...}) — sends session.send RPC; returns messageId.
  10. Events flow from reader goroutine — notification received, unmarshal via result_union.go tag-dispatch, invoke each eventHandlers[i](evt).
  11. Tool invocation (if any) — server→client request arrives; dispatcher finds session, looks up tool, runs wrapped handler createTypedHandler(userFn) (unmarshal JSON→T, call userFn, marshal U→JSON), writes response.
  12. defer client.Stop() — acquires writer lock, sends shutdown, cancels contexts, process.Wait(), falls back to process.Kill() after timeout.

Key Components

FileKey symbolsRole (WHY)
client.goClient, NewClient, Start, Stop, CreateSession, RPCSubprocess + connection + session registry. atomic.Pointer for process handle; RWMutex for lifecycle.
session.goSession, Send, On, Disconnect, UIPer-conversation state, event dispatch, tool handler registry.
types.goAll public types: ClientOptions, SessionConfig, MessageOptions, etc.Single file avoids circular imports; 1213 lines.
definetool.goDefineTool[T, U], generateSchemaForType, createTypedHandlerGeneric tool wrapper — schema via reflection on T, unmarshal in wrapper.
permissions.goExported PermissionHandler struct (13 lines)Namespaces pre-built handlers without polluting package root. copilot.PermissionHandler.ApproveAll.
session_fs_provider.goSessionFsProvider interface + adapterUsers implement an interface; adapter translates Go errors to SessionFSError results.
telemetry.goTraceContextProviderW3C Trace Context plumbing.
process_windows.go / process_other.goPlatform-specific signal handlingBuild-tag split: Windows CTRL_BREAK_EVENT vs Unix SIGTERM.
internal/jsonrpc2/jsonrpc2.goClient, NotificationHandler, RequestHandlerCustom JSON-RPC 2.0 impl for Content-Length-framed stdio.
internal/jsonrpc2/frame.goReadMessage, WriteMessageLSP-style framing: headers then body.
internal/embeddedcli/embeddedcli.goInstaller, EnsureInstalledContent-addressed CLI extraction with flock guard.
internal/flock/flock_unix.goLock, Unlock via unix.FlockPOSIX advisory lock.
internal/flock/flock_windows.goLock, Unlock via LockFileExWindows kernel lock.
internal/flock/flock_other.goNo-op fallbackFor filesystems that don't support advisory locks.
rpc/generated_rpc.goServerRpc, method wrappersCodegen'd from api.schema.json.
rpc/result_union.goHand-written tag-dispatch for event dataCodegen can't yet emit this idiomatically in Go.
cmd/bundler/main.go(executable)Release-time tool: packages CLI into the module. Not part of user builds.
samples/chat.go(executable)Chat demo.

⚡ Algorithm Spotlight

DefineTool — generic tool wrapper

Problem. Users want to write func(args GetWeatherArgs, inv ToolInvocation) (string, error) and have the SDK generate the JSON Schema, unmarshal JSON into T, and marshal U back — without manually writing schema strings.

func DefineTool[T any, U any](name, description string, handler func(T, ToolInvocation) (U, error)) Tool {
    var zero T
    schema := generateSchemaForType(reflect.TypeOf(zero))
    return Tool{
        Name:        name,
        Description: description,
        Parameters:  schema,
        Handler:     createTypedHandler(handler),
    }
}

The clever bit is that T is used twice — once by reflect.TypeOf(zero) to synthesise the schema, and once in the compiled closure createTypedHandler to unmarshal incoming JSON. The generic parameter keeps both in lockstep with compile-time checks.

flock-guarded CLI extraction

Content-addressed cache; double-checked locking; atomic rename. See installer.go. The POSIX advisory lock is automatically released on process exit — no stale-lock cleanup needed.

// Pseudocode from installer.go
target := filepath.Join(cacheDir, "cli-"+contentHash, "copilot")
if fileExists(target) { return target, nil }

lockFile, _ := os.OpenFile(target+".lock", os.O_CREATE|os.O_RDWR, 0644)
defer lockFile.Close()
flock.Lock(lockFile.Fd())
defer flock.Unlock(lockFile.Fd())

if fileExists(target) { return target, nil } // re-check under lock
// extract zip to .tmp, chmod, os.Rename to target

Start/Stop under RWMutex

startStopMux.Lock() for Start/Stop (writer), RLock() for all methods that need a live process. Many CreateSession calls can proceed concurrently; a Stop waits for them to finish before tearing down.

⚠ What Could Go Wrong

FailureDetectionRecoveryEffects
CLI binary not in PATH and not bundledExtraction returns errorStart() returns errorNothing created
Concurrent processes extracting CLIflock loser blocks on lockLoser re-checks after lock; sees extracted copy; uses itOne extracts; others wait briefly
Filesystem doesn't support flockRuntime behaviour (NFS old versions)flock_other.go no-op; atomic rename still prevents most corruptionRare race possible; acceptable on exotic FS
CLI crashes mid-sessionReader goroutine sees io.EOFCloses sessions with error; Client.state = DisconnectedUser must call Stop() and create a new client
Stop called during an in-flight RPCRWMutex blocks Stop writer until readers drainShutdown sent gracefully; timeout then Kill()In-flight RPC may return context.Canceled
Tool handler returns errorWrapped in ToolResult{IsError: true}LLM sees error stringAgent can retry or apologise

📊 Performance Profile

Zero-alloc hot path: JSON decoder reuses bufio.Reader; event dispatch iterates slice. Reflection cost: paid once per tool at DefineTool time; runtime invocations use captured closures. Goroutines: one reader per client + short-lived per-RPC; not thousands. Bundled CLI overhead: first extraction ~20-100ms (disk-bound); subsequent starts skip extraction in O(1).

Deep-Dive Highlights

JSON-RPC Transport Focus

How internal/jsonrpc2 frames messages and bridges two goroutines to the CLI.

Protocol Negotiation Focus

Version handshake in the Go SDK.

Codegen Pipeline Focus

How rpc/generated_rpc.go is produced, and why result_union.go is hand-written.

Configuration Reference

FieldTypeDefaultPurpose
ClientOptions.CLIPathstringbundledOverride binary path
ClientOptions.CLIUrlstring—Connect to external TCP server
ClientOptions.UseStdio*booltruestdio vs TCP transport
ClientOptions.AutoStart*boolfalseAuto-Start on first use
SessionConfig.ModelstringCLI defaultPer-session model
SessionConfig.EnableConfigDiscovery*boolfalseAuto-load .mcp.json
SessionConfig.OnPermissionRequestPermissionHandlerFuncnil (deny)Permission gate
SessionConfig.Tools[]ToolnilCustom tools
SessionConfig.Commands[]CommandDefinitionnilSlash commands
SessionConfig.Hooks*SessionHooksnilpre/post tool use
SessionConfig.SystemMessage*SystemMessageConfignilreplace/customize/transform
SessionConfig.SessionFs*SessionFsConfignilVirtual FS
SessionConfig.ReasoningEffortstring—low/medium/high

Code Examples

Minimal client/session

package main

import (
    "context"
    "fmt"
    "log"
    copilot "github.com/github/copilot-sdk/go"
)

func main() {
    client := copilot.NewClient(nil)
    if err := client.Start(); err != nil { log.Fatal(err) }
    defer client.Stop()

    session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
        OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
    })
    if err != nil { log.Fatal(err) }

    session.On(func(evt copilot.SessionEvent) {
        if d, ok := evt.Data.(*copilot.AssistantMessageData); ok {
            fmt.Println(d.Content)
        }
    })

    session.Send(copilot.MessageOptions{Prompt: "Hello!"})
}

Generic DefineTool

type GetWeatherParams struct {
    City string `json:"city" jsonschema:"city name"`
    Unit string `json:"unit" jsonschema:"temperature unit (celsius or fahrenheit)"`
}

tool := copilot.DefineTool("get_weather", "Get weather for a city",
    func(params GetWeatherParams, inv copilot.ToolInvocation) (string, error) {
        return fmt.Sprintf("Weather in %s: 22°%s", params.City, params.Unit), nil
    })

jsonrpc2 Request/Response types

type Request struct {
    JSONRPC string          `json:"jsonrpc"`
    ID      json.RawMessage `json:"id"`      // nil for notifications
    Method  string          `json:"method"`
    Params  json.RawMessage `json:"params"`
}

type Response struct {
    JSONRPC string          `json:"jsonrpc"`
    ID      json.RawMessage `json:"id,omitempty"`
    Result  json.RawMessage `json:"result,omitempty"`
    Error   *Error          `json:"error,omitempty"`
}

Directory Map

go/ source tree
go/
├── client.go                    ← Client + Start/Stop/CreateSession
├── session.go                   ← Session + event pump + tool dispatch
├── types.go                     ← All public types
├── definetool.go                ← Generic DefineTool[T,U]
├── permissions.go               ← Pre-built permission handlers (13 lines)
├── session_fs_provider.go       ← FS interface + adapter
├── telemetry.go                 ← W3C Trace Context
├── sdk_protocol_version.go      ← Embedded constant
├── process_windows.go           ← Windows CTRL_BREAK signal
├── process_other.go             ← Unix SIGTERM
├── internal/
│   ├── jsonrpc2/
│   │   ├── jsonrpc2.go          ← Client, handlers
│   │   └── frame.go             ← Content-Length framing
│   ├── embeddedcli/
│   │   └── embeddedcli.go       ← Content-addressed extraction
│   ├── flock/
│   │   ├── flock.go             ← Lock interface
│   │   ├── flock_unix.go        ← POSIX advisory
│   │   ├── flock_windows.go     ← LockFileEx
│   │   └── flock_other.go       ← No-op fallback
│   └── e2e/                     ← Conformance tests
├── embeddedcli/
│   └── installer.go             ← Public installer helper
├── rpc/
│   ├── generated_rpc.go         ← Codegen'd ServerRpc
│   └── result_union.go          ← Hand-written event data union
├── cmd/bundler/main.go          ← Release-time packaging
├── samples/chat.go              ← Demo
└── go.mod