| name | github-copilot-sdk |
| description | Authoritative reference for working on MultiTable's GitHub Copilot SDK integration (`@github/copilot-sdk`). Trigger when the user mentions the Copilot SDK, `CopilotClient`, `CopilotSession`, `session.send`, `sendAndWait`, `session.idle`, `assistant.message_delta`, `onPermissionRequest`, `onUserInputRequest`, `onElicitationRequest`, plan / autopilot / chat mode for Copilot, session.abort, `~/.copilot/session-state`, or adding/modifying a Copilot provider under `packages/daemon/src/agent/providers/`. |
| allowed-tools | Read, Grep, Glob, Bash, Edit, Write |
GitHub Copilot SDK reference for MultiTable
Pinned SDK package: @github/copilot-sdk (Node ≥ 20.0.0 per nodejs/package.json engines; the README says 18+ but the engines field is the real floor). Repo: https://github.com/github/copilot-sdk, TypeScript source under nodejs/. The SDK is in public preview. Type signatures and behavior in this skill are quoted from the upstream nodejs/src/* source — re-verify against the locally installed node_modules/@github/copilot-sdk/dist/*.d.ts after any version bump.
This skill is strictly Copilot-only. Do not import Claude Agent SDK names (query, canUseTool, permissionMode: 'plan', Query.interrupt, forkSession) or Codex SDK names (Thread, runStreamed, approvalPolicy, sandboxMode, additionalDirectories) into Copilot code or reasoning. Each SDK has its own primitives — see multitable/claude-codex-comparison.md for the side-by-side.
The two facts that shape everything
-
The SDK is a JSON-RPC client driving a long-lived copilot CLI child process. new CopilotClient() + client.start() spawns the bundled CLI in server mode and opens a vscode-jsonrpc MessageConnection over stdio (or TCP). Multiple CopilotSessions multiplex inside that one child. This is architecturally different from Codex (per-turn codex exec spawn) and Claude (in-process query() async-iterable). One client = one CLI child = many sessions.
-
session.send() returns immediately with a messageId. All progress is push-only via session.on(eventType, handler). There is no AsyncIterable, no Web ReadableStream, no Node EventEmitter. Subscribing returns an unsubscribe function — there is no off(). Drop the unsubscribe and you've leaked a handler for the session's lifetime.
Quick task → file map
Decision tree: which lever to pull?
Need to BLOCK a tool call?
├── Always block, no UI? ─── hooks.onPreToolUse → { permissionDecision: 'deny' }
├── Allow without prompting? ─── defineTool({ ..., skipPermission: true }) OR onPreToolUse → 'allow'
├── Ask the user every time? ─── onPreToolUse → { permissionDecision: 'ask' } → falls through to onPermissionRequest
└── Approve everything (dev)? ─── onPermissionRequest: approveAll (built-in)
Need the agent to ASK the user something?
├── Free-text / multiple choice? ─── onUserInputRequest (UserInputRequest)
├── Structured form / URL? ─── onElicitationRequest (ElicitationContext)
└── Permission for a side effect? ─── onPermissionRequest (PermissionRequest)
ALL THREE are mandatory to wire (or the agent hangs).
Need to RUN code on a lifecycle event?
└── SessionConfig.hooks.onSessionStart / onUserPromptSubmitted /
onPreToolUse / onPostToolUse / onSessionEnd / onErrorOccurred
Need to STOP a turn mid-stream?
└── await session.abort() (separate method; NO AbortSignal on send())
The session is still usable — call send() again.
Need to KILL the session entirely?
└── await session.disconnect() (RPC session.destroy; the object is dead)
Need to RESUME a prior conversation?
└── await client.resumeSession(sessionId, config)
REQUIRES the same sessionId you supplied on createSession.
BYOK keys must be re-supplied; not persisted.
Need PLAN mode / AUTOPILOT / interactive?
└── NATIVE since 1.0.5: send({ prompt, agentMode: 'interactive'|'plan'|'autopilot' }) per send.
Plan's execute gate = SessionConfig.onExitPlanModeRequest. (Pre-1.0.5 recipes in
reference/modes-and-permissions.md are superseded — see pitfalls #12.)
Five rules that get violated most
-
session.idle — and ONLY session.idle — means the agent loop is fully done. assistant.turn_end is the end of one LLM call; the agent loop typically chains many turns (tool → model → tool → model → final). Do not unlock the composer or send the next user message on assistant.turn_end. This was bug #1 in Claude/Codex too — the symptom in Copilot is "user message disappears" because it was steered into a turn that wasn't actually finished.
-
Streaming deltas are ADDITIVE, not cumulative. assistant.message_delta.deltaContent is the chunk to append to your buffer. This is the opposite of Codex (item.updated.item.text is the cumulative body). Mixing the two strategies corrupts the live preview. Always replace your live buffer with assistant.message.content when the canonical message arrives, to absorb any whitespace drift.
-
There are THREE separate "ask the user" channels — onPermissionRequest, onUserInputRequest, onElicitationRequest. Each is a distinct callback on SessionConfig. The agent blocks indefinitely waiting on whichever one you forgot to wire (no host-side timeout). onPermissionRequest is the only one that's mandatory at construction — but if you skip the other two, the agent silently hangs the first time it tries to use them. Wire all three for production.
-
onPermissionRequest is mandatory. Omitting it doesn't disable permission gating — it crashes when the agent first tries to use a tool. Use the exported approveAll for daemon/headless use, or thread it through MultiTable's PermissionManager.
-
Abort is a method, not a signal. send() does not accept an AbortSignal. Cancel via await session.abort(), which fires agent.abort and still fires session.idle afterwards. The session remains valid — you can send() again on the same instance.
Three things to NOT confuse with each other
session.ui.{confirm,select,input,elicitation} is host → agent (you push a UI request into the agent's context).
onUserInputRequest / onElicitationRequest is agent → host (the agent asks you something).
onPermissionRequest is also agent → host but for tool gating specifically (shell, write, read, mcp, url, custom-tool, memory, hook).
Mixing them up is a recipe for a hung session and a confused chat UI.
Ground-truth files
When in doubt about behavior, these are authoritative (in this order):
- The locally installed types, once we install the SDK:
node_modules/@github/copilot-sdk/dist/index.d.ts (and dist/types.d.ts, dist/session.d.ts).
- Generated event taxonomy (currently upstream-only):
nodejs/src/generated/session-events.ts — ~60 typed event variants.
- Generated RPC method list (upstream):
nodejs/src/generated/rpc.ts.
- Upstream sources when the local types are vague:
nodejs/src/client.ts, nodejs/src/session.ts, nodejs/src/types.ts. Use gh api -H "Accept: application/vnd.github.raw" repos/github/copilot-sdk/contents/nodejs/src/<file> for raw reads — GitHub's blob view truncates files >1000 lines.
- Official docs:
https://github.com/github/copilot-sdk/tree/main/docs and https://docs.github.com/en/copilot/how-tos/copilot-sdk/sdk-getting-started.
- Public-preview blog post (concept overview, not API surface):
https://github.blog/news-insights/company-news/build-an-agent-into-any-app-with-the-github-copilot-sdk/.
Where this SDK lives in our codebase
The integration is LIVE (SDK 1.0.5, exact-pinned in packages/daemon/package.json; shipped 2026-07):
packages/daemon/src/
├── agent/providers/copilot.ts ← CopilotAdapter: singleton CopilotClient (lazy start()),
│ per-MT-session CopilotSession cache, native agentMode
│ per send, all three ask channels + onExitPlanModeRequest
└── transcripts/copilotParser.ts ← parses ~/.copilot/session-state/<id>/events.jsonl → Message[]
(NOT checkpoints — see pitfalls #19)
Plus registration in agent/manager.ts (copilot: new CopilotAdapter(permManager, elicitManager)),
hydration branches in manager.ts / api/sessions.ts, catalog wiring (providers/baselines.ts
COPILOT_BASELINE + discovery.ts discoverCopilot via a short-lived client.listModels() probe),
and the AddAgentModal entry. Read pitfalls.md #31 first — the live-verified
1.0.5 gotchas (effort hard-fail + retry, workingDirectory naming, missing re-exports,
reasoningText duplication, rich PermissionRequest union, no autoStart).
The big architectural difference vs. Codex: Copilot has a long-lived child shared across sessions, so the adapter owns the client lifecycle (one start() on first use, stop() on daemon shutdown), not fresh per turn. multitable/integration-plan.md is the historical plan (kept for rationale; the shipped adapter deviates where 1.0.5 made things native — see its status header).