From ed2d719de2e479c59d1074ce96d9445605f80f9a Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 May 2026 08:02:53 -0700 Subject: [PATCH 1/8] docs: refresh for GA; add cloud sessions, fleet mode, multi-tenancy guides - Remove public/technical preview language across root + all SDK READMEs - Match copilot-agent-runtime GA wording (lowercase 'generally available', no banners) - Add docs/setup/multi-tenancy.md, docs/features/cloud-sessions.md, docs/features/fleet-mode.md - Split cloud sessions out of remote-sessions.md - Correct claims: .NET package name, Python install, Java version placeholder + JDK 21+, Go ctx.Context signatures, Node TS 5.2+/Node 20+, Rust permission handler API - Document skipPermission, per-session gitHubToken, BYOK wireApi, assistant.usage.apiEndpoint, fleet-mode compatibility - Mark java ADR-001 superseded by GA Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- README.md | 8 +- docs/auth/authenticate.md | 4 +- docs/auth/byok.md | 8 +- docs/auth/index.md | 6 + docs/features/cloud-sessions.md | 249 ++++++++++++ docs/features/custom-agents.md | 2 +- docs/features/fleet-mode.md | 269 +++++++++++++ docs/features/index.md | 4 +- docs/features/remote-sessions.md | 89 +---- docs/features/streaming-events.md | 3 +- docs/getting-started.md | 78 ++-- docs/hooks/pre-tool-use.md | 17 + docs/index.md | 5 +- docs/observability/index.md | 2 + docs/observability/opentelemetry.md | 2 + docs/setup/backend-services.md | 25 +- docs/setup/choosing-a-setup-path.md | 4 + docs/setup/index.md | 1 + docs/setup/multi-tenancy.md | 357 ++++++++++++++++++ docs/setup/scaling.md | 2 + docs/troubleshooting/compatibility.md | 6 +- docs/troubleshooting/mcp-debugging.md | 2 +- dotnet/README.md | 4 +- go/README.md | 14 +- java/README.md | 18 +- ...adr-001-semver-pre-general-availability.md | 2 + nodejs/README.md | 20 +- nodejs/docs/examples.md | 11 +- python/README.md | 17 +- rust/Cargo.toml | 2 +- rust/README.md | 4 +- 32 files changed, 1049 insertions(+), 188 deletions(-) create mode 100644 docs/features/cloud-sessions.md create mode 100644 docs/features/fleet-mode.md create mode 100644 docs/setup/multi-tenancy.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fa57dbe6..041fc6865 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thanks for your interest in contributing! -This repository contains the Copilot SDK, a set of multi-language SDKs (Node/TypeScript, Python, Go, .NET, Rust) for building applications with the GitHub Copilot agent, maintained by the GitHub Copilot team. +This repository contains the Copilot SDK, a set of multi-language SDKs (Node/TypeScript, Python, Go, .NET, Java, and Rust) for building applications with the GitHub Copilot agent, maintained by the GitHub Copilot team. Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE). diff --git a/README.md b/README.md index ceb7a5b35..19e92e16b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Agents for every app. -Embed Copilot's agentic workflows in your application—now available in public preview as a programmable SDK for Python, TypeScript, Go, .NET, and Java. A Rust SDK is also available in technical preview. +Embed Copilot's agentic workflows in your application with the GitHub Copilot SDK for Python, TypeScript, Go, .NET, Java, and Rust. The GitHub Copilot SDK exposes the same engine behind Copilot CLI: a production-tested agent runtime you can invoke programmatically. No need to build your own orchestration—you define agent behavior, Copilot handles planning, tool invocation, file edits, and more. @@ -37,7 +37,7 @@ Quick steps: 1. **(Optional) Install the Copilot CLI** For Node.js, Python, and .NET SDKs, the Copilot CLI is bundled automatically and no separate installation is required. -For the Go, Java and Rust SDKs, [install the CLI manually](https://github.com/features/copilot/cli) or ensure `copilot` is available in your PATH unless you opt into their application-level CLI bundling features. +For Go, Java, and Rust, [install the CLI manually](https://github.com/features/copilot/cli) or ensure `copilot` is available in your PATH. Go and Rust also expose application-level CLI bundling features. 2. **Install your preferred SDK** using the commands above. @@ -88,7 +88,7 @@ See the **[Authentication documentation](./docs/auth/index.md)** for details on No — for Node.js, Python, and .NET SDKs, the Copilot CLI is bundled automatically as a dependency. You do not need to install it separately. -For Go, Java and Rust SDKs, the CLI is **not** bundled by default. Install the CLI manually, ensure `copilot` is available in your PATH, or opt into their application-level CLI bundling features. +For Go, Java, and Rust SDKs, the CLI is **not** bundled by default. Install the CLI manually or ensure `copilot` is available in your PATH. Go and Rust also expose application-level CLI bundling features. Advanced: You can override the CLI binary or connect to an external server. See the individual SDK README for language-specific options. @@ -117,7 +117,7 @@ All models available via Copilot CLI are supported in the SDK. The SDK also expo ### Is the SDK production-ready? -The GitHub Copilot SDK is currently in Public Preview. While it is functional and can be used for development and testing, it may not yet be suitable for production use. +The GitHub Copilot SDK is generally available and follows semantic versioning. See [CHANGELOG.md](./CHANGELOG.md) for release notes. ### How do I report issues or request features? diff --git a/docs/auth/authenticate.md b/docs/auth/authenticate.md index 36bc855f5..d45e3569b 100644 --- a/docs/auth/authenticate.md +++ b/docs/auth/authenticate.md @@ -300,13 +300,15 @@ BYOK allows you to use your own API keys from model providers like Azure AI Foun When multiple authentication methods are available, the SDK uses them in this priority order: -1. **Explicit `gitHubToken`** - Token passed directly to SDK constructor +1. **Explicit `gitHubToken`** - Token passed directly to the SDK client or session configuration 1. **HMAC key** - `CAPI_HMAC_KEY` or `COPILOT_HMAC_KEY` environment variables 1. **Direct API token** - `GITHUB_COPILOT_API_TOKEN` with `COPILOT_API_URL` 1. **Environment variable tokens** - `COPILOT_GITHUB_TOKEN` → `GH_TOKEN` → `GITHUB_TOKEN` 1. **Stored OAuth credentials** - From previous `copilot` CLI login 1. **GitHub CLI** - `gh auth` credentials +For multi-user server mode, pass a per-session `gitHubToken` so each session runs with the correct GitHub identity; see [Multi-user and server deployments](../setup/multi-tenancy.md). + ## Disabling auto-login To prevent the SDK from automatically using stored credentials or `gh` CLI auth, use the `useLoggedInUser: false` option: diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 8bfc5d50c..9a6adfe77 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -205,15 +205,17 @@ client.stop().get(); | `baseUrl` / `base_url` | string | **Required.** API endpoint URL | | `apiKey` / `api_key` | string | API key (optional for local providers like Ollama) | | `bearerToken` / `bearer_token` | string | Bearer token auth (takes precedence over apiKey) | -| `wireApi` / `wire_api` | `"completions"` \| `"responses"` | API format (default: `"completions"`) | +| `wireApi` / `wire_api` | `"completions"` \| `"responses"` | Select `"completions"` for broad model compatibility (the Chat Completions API); select `"responses"` for multi-turn state management, tool namespacing, and reasoning support (the Responses API). Anthropic models always use the Messages API regardless of this setting. | | `azure.apiVersion` / `azure.api_version` | string | Azure API version (default: `"2024-10-21"`) | ### Wire API format The `wireApi` setting determines which OpenAI API format to use: -* **`"completions"`** (default) - Chat Completions API (`/chat/completions`). Use for most models. -* **`"responses"`** - Responses API. Use for GPT-5 series models that support the newer responses format. +* **`"completions"`** (default) - Chat Completions API (`/chat/completions`) for broad model compatibility. +* **`"responses"`** - Responses API for multi-turn state management, tool namespacing, and reasoning support. + +Anthropic models always use the Anthropic Messages API regardless of this setting. ### Type-specific notes diff --git a/docs/auth/index.md b/docs/auth/index.md index 2d5a3914a..b09646d5d 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -4,3 +4,9 @@ Choose the authentication method that best fits your deployment scenario for the * [Authenticate Copilot SDK](authenticate.md): methods, priority order, and examples * [Bring your own key (BYOK)](./byok.md): use your own API keys from OpenAI, Azure, Anthropic, and more + +## Authentication priority + +When multiple credentials are configured, an explicit SDK token takes priority, followed by HMAC or direct Copilot API environment authentication, environment variable GitHub tokens, stored Copilot CLI credentials, and then GitHub CLI credentials. See [Authenticate Copilot SDK](authenticate.md#authentication-priority) for details. + +For multi-user server mode, pass a per-session `gitHubToken` so each session runs with the correct GitHub identity; see [Multi-user and server deployments](../setup/multi-tenancy.md). diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md new file mode 100644 index 000000000..d87cf0e3b --- /dev/null +++ b/docs/features/cloud-sessions.md @@ -0,0 +1,249 @@ +# Cloud sessions + +Cloud sessions run Copilot work on GitHub-hosted compute through Mission Control. Use them when your app should create a session that executes remotely instead of starting a local Copilot CLI session on the user's machine or your server. + +Cloud sessions are distinct from [remote sessions](./remote-sessions.md). Remote sessions are locally hosted sessions that are also surfaced through Mission Control so users can view and steer them from GitHub web and mobile. Cloud sessions are created with the `cloud` create-session option and are routed by Mission Control to GitHub-hosted compute. + +Cloud sessions are also distinct from the Windows sandbox. The Windows sandbox is local AppContainer tool isolation and does not create GitHub-hosted compute. + +> [!NOTE] +> Don't confuse cloud sessions with the Windows sandbox. The Windows sandbox is a local AppContainer for tool isolation, enabled with `SANDBOX=true`. They are unrelated features. + +## Prerequisites + +Before creating a cloud session, make sure: + +* The user has Copilot access with cloud-agent entitlement. +* The session can authenticate to GitHub, either with a user token or a logged-in Copilot CLI identity. +* You can associate the session with a GitHub repository. This is optional in the SDK type, but recommended so Mission Control and the cloud agent have repository context. +* Organization policies allow remote control and viewing sessions from cloud surfaces. + +## Creating a cloud session + +Set the create-session `cloud` option to create a cloud session. You can include repository metadata to associate the cloud session with a GitHub repository. + + + +### TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + onPermissionRequest: async () => ({ kind: "approve-once" }), + cloud: { + repository: { + owner: "github", + name: "copilot-sdk", + branch: "main", + }, + }, +}); +``` + +### Python + + +```python +from copilot import CopilotClient, CloudSessionOptions, CloudSessionRepository +from copilot.session import PermissionHandler + +client = CopilotClient() +await client.start() + +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", + name="copilot-sdk", + branch="main", + ) + ), +) +``` + +### Go + + +```go +client := copilot.NewClient(nil) +if err := client.Start(ctx); err != nil { + return err +} + +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Cloud: &copilot.CloudSessionOptions{ + Repository: &copilot.CloudSessionRepository{ + Owner: "github", + Name: "copilot-sdk", + Branch: "main", + }, + }, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil + }, +}) +_ = session +``` + +### .NET + + +```csharp +await using var client = new CopilotClient(); + +var session = await client.CreateSessionAsync(new SessionConfig +{ + Cloud = new CloudSessionOptions + { + Repository = new CloudSessionRepository + { + Owner = "github", + Name = "copilot-sdk", + Branch = "main", + }, + }, + OnPermissionRequest = (req, inv) => + Task.FromResult(PermissionDecision.ApproveOnce()), +}); +``` + +### Java + + +```java +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; + +try (var client = new CopilotClient()) { + client.start().get(); + + var session = client.createSession( + new SessionConfig() + .setCloud(new CloudSessionOptions() + .setRepository(new CloudSessionRepository() + .setOwner("github") + .setName("copilot-sdk") + .setBranch("main"))) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + ).get(); +} +``` + +### Rust + + +```rust +use github_copilot_sdk::{CloudSessionOptions, CloudSessionRepository, SessionConfig}; +use github_copilot_sdk::handler::PermissionResult; + +let session = client.create_session( + SessionConfig::default() + .with_cloud(CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + )) + .with_permission_handler(|_req, _inv| async { + Ok(PermissionResult::approve_once()) + }), +).await?; +``` + + + +## Repository association + +The `cloud.repository` object associates the cloud session with a GitHub repository: + +| Field | Required | Description | +|-------|----------|-------------| +| `owner` | Yes | Repository owner or organization. | +| `name` | Yes | Repository name. | +| `branch` | No | Branch to use for repository context. Omit it to let the runtime choose the default branch or current repository context. | + +Repository association is optional in the SDK type, but include it whenever your app knows the target repository. It helps Mission Control display the session in the right context and gives the cloud agent a clearer starting point. + +Use `branch` when the work should start from a specific branch. If your app is creating sessions from pull requests, issue triage flows, or deployment workflows, pass the branch that matches the user-visible task. + +## Resuming a cloud session + +The `cloud` option only applies when creating a new session. To resume an existing cloud session, use the standard resume API for the SDK language: + + +```typescript +const session = await client.resumeSession("session-id"); +``` + +Do not pass `cloud` again on resume. The saved session metadata determines that the session is cloud-backed, and resume follows the normal session resume path. + +## Org policies and entitlements + +Cloud session creation can fail when the user or organization is not entitled to cloud-agent execution or when organization-level policies block the flow. In particular, policies for remote control or viewing sessions from cloud surfaces can prevent Mission Control from creating the cloud task. + +When this happens, the runtime reports a `"policy_blocked"` failure reason for cloud task creation. Treat this as an authorization or policy outcome, not as a transient infrastructure failure. + +In TypeScript, check for the reason before retrying: + + +```typescript +try { + await client.createSession({ cloud: { repository } }); +} catch (error) { + if ((error as { reason?: string }).reason === "policy_blocked") { + // Show an admin-facing message or link to org policy settings. + } + throw error; +} +``` + +In languages where SDK errors are represented differently, inspect the surfaced error reason or code and handle `"policy_blocked"` explicitly. Retrying without a policy change is not expected to succeed. + +## Integration ID and routing + +Cloud sessions are stamped with a `Copilot-Integration-Id` header derived from the `GITHUB_COPILOT_INTEGRATION_ID` environment variable. This integration ID is used by Mission Control for routing, attribution, and integration-specific behavior. + +For multi-user server guidance and full integration ID details, see [Multi-tenancy](../setup/multi-tenancy.md). + +Mission Control routes SDK-created cloud sessions to the `copilot-developer-sandbox` agent slug. The name is an internal routing slug for the cloud agent and does not mean the session uses the local Windows sandbox. + +## Advanced: `COPILOT_MC_BASE_URL` + +By default, the runtime derives the Mission Control base URL from the configured Copilot API URL. Set `COPILOT_MC_BASE_URL` only when you need to override that Mission Control endpoint. + +This may be required for GitHub Enterprise Server deployments. Confirm the correct value and support status with your GitHub representative before relying on it in production. + + +```shell +COPILOT_MC_BASE_URL="https://example.com/agents" +``` + +## Cloud sessions vs. remote sessions + +| Capability | Remote sessions | Cloud sessions | +|------------|-----------------|----------------| +| Execution location | Local machine or your server | GitHub-hosted compute | +| Mission Control role | Shares a local session to GitHub web/mobile | Creates and routes the hosted session | +| SDK option | `remote: true` on the client or session | `cloud: { ... }` on create session | +| Resume path | Standard resume | Standard resume | +| Windows sandbox relation | Unrelated | Unrelated | + +Use remote sessions when the session should execute where the SDK runtime is already running, but also be accessible from Mission Control. Use cloud sessions when the session should execute on GitHub-hosted compute. + +## Troubleshooting + +| Symptom | Likely cause | What to check | +|---------|--------------|---------------| +| Cloud session creation returns `"policy_blocked"` | Organization policy blocks remote control or view from cloud flows | Check org Copilot policies and user entitlement | +| Session creates without repository context | `cloud.repository` was omitted | Pass `owner`, `name`, and optionally `branch` | +| Resume ignores a new `cloud` option | `cloud` only applies to new sessions | Resume the existing session normally | +| Confusion with sandbox settings | Windows sandbox and cloud sessions are separate | Do not use `SANDBOX=true` for cloud execution | + +## See also + +* [Remote Sessions](./remote-sessions.md): share locally hosted sessions through Mission Control +* [Multi-tenancy](../setup/multi-tenancy.md): integration IDs and server deployment patterns +* [Authentication](../auth/index.md): configure GitHub authentication for SDK sessions diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index d0f209649..fb2f81fd1 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -1,6 +1,6 @@ # Custom agents and sub-agent orchestration -Define specialized agents with scoped tools and prompts, then let Copilot orchestrate them as sub-agents within a single session. +Define specialized agents with scoped tools and prompts, then let Copilot orchestrate them as sub-agents within a single session. For dispatching multiple sub-agents in parallel, see [Fleet Mode](./fleet-mode.md). ## Overview diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md new file mode 100644 index 000000000..51732ce7d --- /dev/null +++ b/docs/features/fleet-mode.md @@ -0,0 +1,269 @@ +# Fleet mode + +Fleet mode is Copilot's parallel orchestration pattern for work that can be split across independent sub-agents. In the runtime research notes, fleet mode is described as "the runtime's built-in pattern for dispatching multiple sub-agents in parallel via the `task` tool, with SQL todos as the shared coordination state." Use it when one parent session should coordinate several workers, collect their results, and continue the conversation with the combined context. + +## When to use fleet mode + +Fleet mode is useful when the work can be decomposed before execution and each unit can run without waiting for the others. + +Good fits include: + +- Multi-file refactors where each worker owns a file, package, or language SDK. +- Batch reviews where each worker checks a separate diff, module, or alert group. +- Parallel research across independent repositories, services, or feature areas. +- Documentation refreshes where each worker owns a page or topic. +- Migration tasks where each worker can validate its own slice and report back. + +Avoid fleet mode for: + +- Sequential tasks where step 2 needs the concrete output from step 1. +- Tightly coupled edits where workers would contend for the same files. +- Small tasks that one synchronous sub-agent or the parent agent can finish quickly. +- Tasks that require continuous shared reasoning rather than clear ownership. + +Fleet mode works best when the parent session can create clear units of work, assign one owner per unit, and define what each worker must return. + +## Starting fleet mode + +The SDK exposes fleet mode through the session RPC namespace in several languages. The binding is experimental in the generated RPC surface; pin both the SDK and the Copilot CLI runtime if your application depends on it. + +### From within a session + +The wire method is `session.fleet.start`. The optional `prompt` is combined with the runtime's fleet orchestration instructions. + +
+Node.js / TypeScript + +```typescript +const result = await session.rpc.fleet.start({ + prompt: "Refactor each SDK package independently, then summarize the changes.", +}); + +if (result.started) { + console.log("Fleet mode started"); +} +``` + +
+ +
+Python + +```python +from copilot.generated.rpc import FleetStartRequest + +result = await session.rpc.fleet.start( + FleetStartRequest( + prompt="Review each service independently, then summarize the risks." + ) +) + +if result.started: + print("Fleet mode started") +``` + +
+ +
+Go + +```go +prompt := "Update each package independently, then report validation results." +result, err := session.RPC.Fleet.Start(ctx, &rpc.FleetStartRequest{ + Prompt: &prompt, +}) +if err != nil { + return err +} +if result.Started { + fmt.Println("Fleet mode started") +} +``` + +
+ +
+.NET + +```csharp +var result = await session.Rpc.Fleet.StartAsync( + "Audit each project independently, then summarize the findings."); + +if (result.Started) +{ + Console.WriteLine("Fleet mode started"); +} +``` + +
+ +
+Rust + +```rust +use github_copilot::generated::api_types::FleetStartRequest; + +let result = session + .rpc() + .fleet() + .start(FleetStartRequest { + prompt: Some("Research each crate independently, then summarize the plan.".into()), + }) + .await?; + +if result.started { + println!("Fleet mode started"); +} +``` + +
+ +Native typed bindings for fleet mode were verified in Node.js/TypeScript, Python, Go, .NET, and Rust. A Java binding was not found in `java/src/main/java` on this branch, so Java examples are omitted until that surface is available. + +### From plan mode + +Plan-mode UIs can start fleet deployment by returning the `autopilot_fleet` exit action. The generated session event types describe it as: + +```typescript +/** Exit plan mode and continue with parallel autonomous workers. */ +| "autopilot_fleet"; +``` + +Use this when a user approves a plan that already contains independent work items. Use `autopilot` for a single autonomous worker and `interactive` when the user should stay in the loop. + +## How sub-agents coordinate + +Fleet mode relies on explicit coordination state instead of implicit shared memory. The parent agent decomposes the work into todos, each sub-agent owns one todo, and the orchestrator dispatches workers whose dependencies are already complete. + +The canonical schema is: + +```sql +CREATE TABLE todos ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'pending' +); + +CREATE TABLE todo_deps ( + todo_id TEXT, + depends_on TEXT, + PRIMARY KEY (todo_id, depends_on) +); +``` + +Each todo moves through a small state machine: + +```text +pending -> in_progress -> done + \-> blocked +``` + +A sub-agent should: + +1. Claim exactly one ready todo by setting `status = 'in_progress'`. +1. Work only on that todo's scope. +1. Store its result in the conversation or relevant task output. +1. Set `status = 'done'` when complete. +1. Set `status = 'blocked'` when it cannot proceed, and include the reason. + +The orchestrator can find work whose dependencies are satisfied with a query like: + +```sql +SELECT t.* +FROM todos t +WHERE t.status = 'pending' + AND NOT EXISTS ( + SELECT 1 + FROM todo_deps td + JOIN todos dep ON td.depends_on = dep.id + WHERE td.todo_id = t.id + AND dep.status != 'done' + ); +``` + +This pattern gives every worker a clear owner and lets the parent session reason about what is ready, running, complete, or blocked. + +## Lifecycle hooks + +Fleet mode invokes sub-agents through the runtime's task mechanism. The runtime emits hook activity for sub-agent tool calls: the runtime 1.0.52 changelog notes that `preToolUse`, `postToolUse`, `subagentStart`, and `subagentStop` fire correctly for sub-agent tool calls. + +A dedicated SDK hook callback for `subagentStart` or `subagentStop` was not found in the public SDK surface on this branch. SDK consumers can observe sub-agent activity through the generic session event stream, which includes events such as `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`. + +
+Node.js / TypeScript + +```typescript +session.on((event) => { + if (event.type === "subagent.started") { + console.log(`Started ${event.data.agentDisplayName}`); + } + + if (event.type === "subagent.completed") { + console.log(`Completed ${event.data.agentDisplayName}`); + } +}); +``` + +
+ +
+Python + +```python +def handle_event(event): + if event.type == "subagent.started": + print(f"Started {event.data.agent_display_name}") + elif event.type == "subagent.completed": + print(f"Completed {event.data.agent_display_name}") + +unsubscribe = session.on(handle_event) +``` + +
+ +For hook configuration that is already exposed at the SDK layer, see [Hooks](hooks.md). For sub-agent event payloads, see [Custom agents and sub-agent orchestration](custom-agents.md). + +## Plugin sub-agents + +The runtime can load plugins with `--plugin-dir`. Plugins loaded this way can register their agents as available `task(agent_type=...)` sub-agent types in prompt mode, which means fleet mode can dispatch to those plugin-provided worker types. + +This is currently a runtime-level configuration pattern rather than a documented SDK-level registration API. Configure the Copilot CLI runtime with the plugin directory, then connect the SDK client to that runtime. Native SDK helpers for registering plugin sub-agent types may be added in the future. + +Conceptually, a fleet prompt can then ask for a specific worker type: + +```text +Use task(agent_type="security-review") for each independent package. +Run the workers in parallel and summarize only high-confidence findings. +``` + +Keep plugin-provided sub-agent types narrow and descriptive so the orchestrator can choose them reliably. + +## Best practices + +- Decompose the work into independent units before starting fleet mode. +- Minimize dependencies between todos; dependencies reduce parallelism. +- Give each todo a durable ID, a clear title, and a complete description. +- Make each sub-agent own exactly one todo at a time. +- Use background sub-agents for truly parallel work. +- Use synchronous sub-agent calls for serialized steps or validation gates. +- Provide each sub-agent with complete context; sub-agents are stateless across calls. +- Include file paths, commands, expected outputs, and constraints in each worker prompt. +- Do not dispatch a single background sub-agent; prefer a synchronous call or batch multiple workers in parallel. +- Avoid assigning overlapping files to different workers unless the parent agent will reconcile conflicts explicitly. +- Require every worker to report what it changed, how it validated the change, and what remains blocked. +- Have the parent agent verify the combined result after workers finish. + +## Limitations and open questions + +- Fleet mode is exposed through generated session RPC bindings and is marked experimental in several SDKs. +- The SQL todos pattern is the canonical coordination model in the runtime guidance, but whether it is a stable extensibility contract for SDK consumers is still an open question. +- `subagentStart` and `subagentStop` are runtime hook names; this branch exposes sub-agent lifecycle to SDK consumers through the generic session event stream, not dedicated hook callbacks. +- Plugin sub-agent registration is configured at the runtime layer through `--plugin-dir`; no SDK-level plugin registration helper was verified on this branch. +- Java native typed bindings for `session.fleet.start` were not found in the Java SDK source on this branch. +- Fleet mode does not remove the need for parent-agent review. Parallel workers can produce inconsistent assumptions that the orchestrator must reconcile. + +## See also + +- [Custom agents and sub-agent orchestration](custom-agents.md) +- [Hooks](hooks.md) diff --git a/docs/features/index.md b/docs/features/index.md index 2fa11b76e..6ea21e364 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -11,13 +11,15 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [The Agent Loop](./agent-loop.md) | How the CLI processes a prompt—the tool-use loop, turns, and completion signals | | [Hooks](./hooks.md) | Intercept and customize session behavior—control tool execution, transform results, handle errors | | [Custom Agents](./custom-agents.md) | Define specialized sub-agents with scoped tools and instructions | +| [Fleet Mode](./fleet-mode.md) | Dispatch multiple sub-agents in parallel for large, independent workstreams | | [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access | | [Skills](./skills.md) | Load reusable prompt modules from directories | | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing | | [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage | -| [Remote Sessions](./remote-sessions.md) | Share sessions to GitHub web and mobile via Mission Control | +| [Remote Sessions](./remote-sessions.md) | Share locally hosted sessions to GitHub web and mobile via Mission Control | +| [Cloud Sessions](./cloud-sessions.md) | Run sessions on GitHub-hosted compute through Mission Control | ## Related diff --git a/docs/features/remote-sessions.md b/docs/features/remote-sessions.md index f58238eee..5a15ca825 100644 --- a/docs/features/remote-sessions.md +++ b/docs/features/remote-sessions.md @@ -2,6 +2,8 @@ Remote sessions let users access their Copilot session from GitHub web and mobile via [Mission Control](https://github.com). When enabled, the SDK connects each session to Mission Control, producing a URL that can be shared as a link or QR code. +For running sessions on GitHub-hosted compute, see [Cloud Sessions](./cloud-sessions.md). + ## Prerequisites * The user must be authenticated (GitHub token or logged-in user) @@ -120,92 +122,6 @@ while let Ok(event) = events.recv().await { -### Cloud sessions - -Set the create-session `cloud` option to create a remote session in the cloud instead of a local session. You can include repository metadata to associate the cloud session with a GitHub repository. - - - -#### TypeScript - - -```typescript -const session = await client.createSession({ - onPermissionRequest: async () => ({ allowed: true }), - cloud: { - repository: { owner: "github", name: "copilot-sdk", branch: "main" }, - }, -}); -``` - -#### Python - - -```python -from copilot import CloudSessionOptions, CloudSessionRepository - -session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - cloud=CloudSessionOptions( - repository=CloudSessionRepository( - owner="github", - name="copilot-sdk", - branch="main", - ) - ), -) -``` - -#### Go - - -```go -session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Cloud: &copilot.CloudSessionOptions{ - Repository: &copilot.CloudSessionRepository{ - Owner: "github", - Name: "copilot-sdk", - Branch: "main", - }, - }, -}) -``` - -#### C# - - -```csharp -var session = await client.CreateSessionAsync(new SessionConfig -{ - Cloud = new CloudSessionOptions - { - Repository = new CloudSessionRepository - { - Owner = "github", - Name = "copilot-sdk", - Branch = "main" - } - } -}); -``` - -#### Rust - - -```rust -use github_copilot_sdk::{CloudSessionOptions, CloudSessionRepository, SessionConfig}; - -let session = client.create_session( - SessionConfig::default().with_cloud( - CloudSessionOptions::with_repository( - CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), - ), - ), -).await?; -``` - - - ### On-demand (per-session toggle) Use `session.rpc.remote.enable()` to start remote access mid-session, and `session.rpc.remote.disable()` to stop it. This is equivalent to the CLI's `/remote on` and `/remote off` commands. @@ -286,6 +202,5 @@ The remote URL can be rendered as a QR code for easy mobile access. The SDK prov ## Notes * The `remote` client option only applies when the SDK spawns the CLI process. It is ignored when connecting to an external server via `cliUrl`. -* The `cloud` session option applies only to new sessions created with `session.create`; it is not used when resuming an existing session. * If the working directory is not a GitHub repository, remote setup is silently skipped (always-on mode) or returns an error (on-demand mode). * Remote sessions require authentication. Ensure `gitHubToken` or `useLoggedInUser` is configured. diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 3388d6a1a..460b4918e 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -315,6 +315,7 @@ Ephemeral. Token usage and cost information for an individual API call. | `duration` | `number` | | API call duration in milliseconds | | `initiator` | `string` | | What triggered this call (e.g., `"sub-agent"`); absent for user-initiated | | `apiCallId` | `string` | | Completion ID from the provider (e.g., `chatcmpl-abc123`) | +| `apiEndpoint` | `"/chat/completions" \| "/v1/messages" \| "/responses"` | | API endpoint used for the model call; useful for observability and cost attribution | | `providerCallId` | `string` | | GitHub request tracing ID (`x-github-request-id`) | | `parentToolCallId` | `string` | | Set when usage originates from a sub-agent | | `quotaSnapshots` | `Record` | | Per-quota resource usage, keyed by quota identifier | @@ -759,7 +760,7 @@ session.idle → Ready for next message (ephemeral) | `assistant.message` | | Assistant | `messageId`, `content`, `toolRequests?`, `outputTokens?`, `phase?` | | `assistant.message_delta` | ✅ | Assistant | `messageId`, `deltaContent`, `parentToolCallId?` | | `assistant.turn_end` | | Assistant | `turnId` | -| `assistant.usage` | ✅ | Assistant | `model`, `inputTokens?`, `outputTokens?`, `cost?`, `duration?` | +| `assistant.usage` | ✅ | Assistant | `model`, `apiEndpoint?`, `inputTokens?`, `outputTokens?`, `cost?`, `duration?` | | `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` | | `tool.execution_start` | | Tool | `toolCallId`, `toolName`, `arguments?`, `mcpServerName?` | | `tool.execution_partial_result` | ✅ | Tool | `toolCallId`, `partialOutput` | diff --git a/docs/getting-started.md b/docs/getting-started.md index 80c9541e4..260267e82 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -18,7 +18,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside! Before you begin, make sure you have: -* **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)) +* **GitHub Copilot CLI** installed and authenticated (the Node.js, Python, and .NET SDKs bundle the CLI automatically—see [Bundled CLI](./setup/bundled-cli.md). Required for Go, Java, and Rust unless using their application-level CLI bundling features.) * Your preferred language runtime: * **Node.js** 20+ or **Python** 3.11+ or **Go** 1.24+ or **Rust** 1.94+ or **Java** 17+ or **.NET** 8.0+ @@ -264,7 +264,7 @@ use github_copilot_sdk::{Client, ClientOptions, MessageOptions, SessionConfig}; async fn main() -> Result<(), Box> { let client = Client::start(ClientOptions::default()).await?; let session = client - .create_session(SessionConfig::default().with_handler(Arc::new(ApproveAllHandler))) + .create_session(SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler))) .await?; let response = session @@ -514,7 +514,7 @@ async fn main() -> Result<(), Box> { let mut config = SessionConfig::default(); config.streaming = Some(true); let session = client - .create_session(config.with_handler(Arc::new(ApproveAllHandler))) + .create_session(config.with_permission_handler(Arc::new(ApproveAllHandler))) .await?; // Listen for response chunks @@ -1086,7 +1086,7 @@ use std::sync::Arc; use std::time::Duration; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{JsonSchema, ToolHandlerRouter, define_tool}; +use github_copilot_sdk::tool::{define_tool, JsonSchema}; use github_copilot_sdk::{Client, ClientOptions, MessageOptions, SessionConfig, ToolResult}; use serde::Deserialize; @@ -1098,27 +1098,28 @@ struct GetWeatherParams { #[tokio::main] async fn main() -> Result<(), Box> { // Define a tool that Copilot can call - let router = ToolHandlerRouter::new( - vec![define_tool( - "get_weather", - "Get the current weather for a city", - |_inv, params: GetWeatherParams| async move { - Ok(ToolResult::Text(format!( - "{}: 62°F and sunny", - params.city - ))) - }, - )], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let tools = vec![define_tool( + "get_weather", + "Get the current weather for a city", + |_inv, params: GetWeatherParams| async move { + Ok(ToolResult::Text(format!( + "{}: 62°F and sunny", + params.city + ))) + }, + )]; let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.streaming = Some(true); - config.tools = Some(tools); - let session = client.create_session(config.with_handler(Arc::new(router))).await?; + let session = client + .create_session( + config + .with_tools(tools) + .with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await?; let mut events = session.subscribe(); tokio::spawn(async move { @@ -1546,7 +1547,7 @@ use std::sync::Arc; use std::time::Duration; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{JsonSchema, ToolHandlerRouter, define_tool}; +use github_copilot_sdk::tool::{define_tool, JsonSchema}; use github_copilot_sdk::{Client, ClientOptions, MessageOptions, SessionConfig, ToolResult}; use serde::Deserialize; @@ -1567,27 +1568,28 @@ fn read_line() -> Option { #[tokio::main] async fn main() -> Result<(), Box> { - let router = ToolHandlerRouter::new( - vec![define_tool( - "get_weather", - "Get the current weather for a city", - |_inv, params: GetWeatherParams| async move { - Ok(ToolResult::Text(format!( - "{}: 62°F and sunny", - params.city - ))) - }, - )], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let tools = vec![define_tool( + "get_weather", + "Get the current weather for a city", + |_inv, params: GetWeatherParams| async move { + Ok(ToolResult::Text(format!( + "{}: 62°F and sunny", + params.city + ))) + }, + )]; let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.streaming = Some(true); - config.tools = Some(tools); - let session = client.create_session(config.with_handler(Arc::new(router))).await?; + let session = client + .create_session( + config + .with_tools(tools) + .with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await?; let mut events = session.subscribe(); tokio::spawn(async move { @@ -2054,7 +2056,7 @@ let client = Client::start(options).await?; // Use the client normally let session = client - .create_session(SessionConfig::default().with_handler(Arc::new(ApproveAllHandler))) + .create_session(SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler))) .await?; // ... ``` diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index fe373cc2f..c2ab0b0ba 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -154,6 +154,23 @@ Return `null` or `undefined` to allow the tool to execute with no changes. Other | `"deny"` | Tool is blocked, reason shown to user | | `"ask"` | User is prompted to approve (interactive mode) | +### Skipping permission prompts for trusted custom tools + +If you define a custom tool that is safe to run without prompting, set `skipPermission: true` on the tool definition. Use this for trusted, app-owned tools whose inputs are already constrained by your application; use `onPreToolUse` when you need per-call policy checks or argument validation. + +```typescript +const getWeather = defineTool("get_weather", { + description: "Get weather for a location.", + parameters: { + type: "object", + properties: { location: { type: "string" } }, + required: ["location"], + }, + skipPermission: true, + handler: async ({ location }) => ({ forecast: `Sunny in ${location}` }), +}); +``` + ## Examples ### Allow all tools (logging only) diff --git a/docs/index.md b/docs/index.md index 059b54b12..276d8d1e1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,7 @@ How to configure and deploy the SDK for your use case. * [GitHub OAuth](./setup/github-oauth.md): implement the OAuth flow * [Azure Managed Identity](./setup/azure-managed-identity.md): BYOK with Azure AI Foundry * [Scaling & Multi-Tenancy](./setup/scaling.md): horizontal scaling, isolation patterns +* [Multi-Tenancy & Server Deployments](./setup/multi-tenancy.md): mode: "empty", session isolation, integration IDs, sessionFs ### [Authentication](./auth/index.md) @@ -48,7 +49,9 @@ Guides for building with the SDK's capabilities. * [Streaming Events](./features/streaming-events.md): real-time event reference * [Steering & Queueing](./features/steering-and-queueing.md): message delivery modes * [Session Persistence](./features/session-persistence.md): resume sessions across restarts -* [Remote Sessions](./features/remote-sessions.md): share sessions to GitHub web and mobile +* [Remote Sessions](./features/remote-sessions.md): share sessions to GitHub web and mobile via Mission Control +* [Cloud Sessions](./features/cloud-sessions.md): run sessions on GitHub-hosted compute with the cloud: option +* [Fleet Mode](./features/fleet-mode.md): dispatch parallel sub-agents for parallelizable work ### [Hooks Reference](./hooks/index.md) diff --git a/docs/observability/index.md b/docs/observability/index.md index 9859cdffd..3f75e4658 100644 --- a/docs/observability/index.md +++ b/docs/observability/index.md @@ -3,3 +3,5 @@ Monitor and debug your GitHub Copilot SDK applications. * [OpenTelemetry instrumentation](./opentelemetry.md): built-in TelemetryConfig and trace context propagation + +For cost attribution and endpoint-level analysis, subscribe to `assistant.usage` events and inspect `apiEndpoint` (`AssistantUsageApiEndpoint`); see [Streaming events](../features/streaming-events.md). diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index ee2014efb..d89f68ca9 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -115,6 +115,8 @@ let client = Client::start(ClientOptions::new() The SDK can propagate W3C Trace Context (`traceparent`/`tracestate`) on JSON-RPC payloads so that your application's spans and the CLI's spans are linked in one distributed trace. This is useful when, for example, you want to see a "handle tool call" span in your app nested inside the CLI's "execute tool" span, or show the SDK call as a child of your request-handling span. +For cost attribution alongside traces, subscribe to `assistant.usage` events and inspect `apiEndpoint` (`AssistantUsageApiEndpoint`) to see whether a turn used Chat Completions, Responses, or Anthropic Messages; see [Streaming events](../features/streaming-events.md). + #### SDK → CLI (outbound) For **Node.js**, provide an `onGetTraceContext` callback on the client options. This is only needed if your application already uses `@opentelemetry/api` and you want to link your spans with the CLI's spans. The SDK calls this callback before `session.create`, `session.resume`, and `session.send` RPCs: diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index 2dc2c47d1..8cbc8d27a 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -36,6 +36,8 @@ flowchart TB * Multiple SDK clients can share one CLI server * Works with any auth method (GitHub tokens, env vars, BYOK) +For multi-user server mode, configure SDK clients with `mode: "empty"`, pass user credentials per session, and explicitly allow tools for each session. See [Multi-Tenancy & Server Deployments](./multi-tenancy.md) for the full pattern. + ## Architecture: auto-managed vs. external CLI ```mermaid @@ -127,11 +129,14 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient({ cliUrl: "localhost:4321", + mode: "empty", }); const session = await client.createSession({ sessionId: `user-${userId}-${Date.now()}`, model: "gpt-4.1", + availableTools: ["custom:*"], + gitHubToken: user.githubToken, }); const response = await session.sendAndWait({ prompt: req.body.message }); @@ -318,17 +323,18 @@ copilot --headless --port 4321 Pass individual user tokens when creating sessions. See [GitHub OAuth](./github-oauth.md) for the full flow. ```typescript +const client = new CopilotClient({ + cliUrl: "localhost:4321", + mode: "empty", +}); + // Your API receives user tokens from your auth layer app.post("/chat", authMiddleware, async (req, res) => { - const client = new CopilotClient({ - cliUrl: "localhost:4321", - gitHubToken: req.user.githubToken, - useLoggedInUser: false, - }); - const session = await client.createSession({ sessionId: `user-${req.user.id}-chat`, model: "gpt-4.1", + availableTools: ["custom:*"], + gitHubToken: req.user.githubToken, }); const response = await session.sendAndWait({ @@ -385,9 +391,10 @@ import { CopilotClient } from "@github/copilot-sdk"; const app = express(); app.use(express.json()); -// Single shared CLI connection +// Single shared CLI connection for multi-user server mode const client = new CopilotClient({ cliUrl: process.env.CLI_URL || "localhost:4321", + mode: "empty", }); app.post("/api/chat", async (req, res) => { @@ -401,6 +408,8 @@ app.post("/api/chat", async (req, res) => { session = await client.createSession({ sessionId, model: "gpt-4.1", + availableTools: ["custom:*"], + gitHubToken: req.user.githubToken, }); } @@ -538,11 +547,13 @@ setInterval(() => cleanupSessions(24 * 60 * 60 * 1000), 60 * 60 * 1000); | Need | Next Guide | |------|-----------| | Multiple CLI servers / high availability | [Scaling & Multi-Tenancy](./scaling.md) | +| SDK isolation for concurrent users | [Multi-Tenancy & Server Deployments](./multi-tenancy.md) | | GitHub account auth for users | [GitHub OAuth](./github-oauth.md) | | Your own model keys | [BYOK](../auth/byok.md) | ## Next steps +* **[Multi-Tenancy & Server Deployments](./multi-tenancy.md)**: Configure SDK isolation for concurrent users * **[Scaling & Multi-Tenancy](./scaling.md)**: Handle more users, add redundancy * **[Session Persistence](../features/session-persistence.md)**: Resume sessions across restarts * **[GitHub OAuth](./github-oauth.md)**: Add user authentication diff --git a/docs/setup/choosing-a-setup-path.md b/docs/setup/choosing-a-setup-path.md index f1c4636c6..7fe28be8d 100644 --- a/docs/setup/choosing-a-setup-path.md +++ b/docs/setup/choosing-a-setup-path.md @@ -50,6 +50,7 @@ You're building tools for your team or company. Users are employees who need to 1. **[Backend Services](./backend-services.md)**—Run the SDK in your internal services **If scaling beyond a single server:** +1. **[Multi-tenancy and server deployments](./multi-tenancy.md)**—Configure SDK options for multi-user server mode 1. **[Scaling & Multi-Tenancy](./scaling.md)**—Handle multiple users and services ### 🚀 App developer (ISV) @@ -62,6 +63,7 @@ You're building a product for customers. You need to handle authentication for y 1. **[Backend Services](./backend-services.md)**—Power your product from server-side code **For production:** +1. **[Multi-tenancy and server deployments](./multi-tenancy.md)**—Use `mode: "empty"`, per-session tokens, and isolated runtime state 1. **[Scaling & Multi-Tenancy](./scaling.md)**—Serve many customers reliably ### 🏗️ Platform developer @@ -70,6 +72,7 @@ You're embedding Copilot into a platform—APIs, developer tools, or infrastruct **Start with:** 1. **[Backend Services](./backend-services.md)**—Core server-side integration +1. **[Multi-tenancy and server deployments](./multi-tenancy.md)**—SDK-level isolation, per-session auth, and shared runtime options 1. **[Scaling & Multi-Tenancy](./scaling.md)**—Session isolation, horizontal scaling, persistence **Depending on your auth model:** @@ -88,6 +91,7 @@ Use this table to find the right guides based on what you need to do: | Use your own model keys (OpenAI, Azure, etc.) | [BYOK](../auth/byok.md) | | Azure BYOK with Managed Identity (no API keys) | [Azure Managed Identity](./azure-managed-identity.md) | | Run the SDK on a server | [Backend Services](./backend-services.md) | +| Configure SDK options for concurrent users | [Multi-tenancy and server deployments](./multi-tenancy.md) | | Serve multiple users / scale horizontally | [Scaling & Multi-Tenancy](./scaling.md) | ## Configuration comparison diff --git a/docs/setup/index.md b/docs/setup/index.md index d077cc6bb..cc4183ca7 100644 --- a/docs/setup/index.md +++ b/docs/setup/index.md @@ -6,6 +6,7 @@ Configure and deploy the GitHub Copilot SDK for your use case. * [Default setup (bundled CLI)](./bundled-cli.md): the SDK includes the CLI automatically * [Local CLI](./local-cli.md): use your own CLI binary or running instance * [Backend services](./backend-services.md): server-side with headless CLI over TCP +* [Multi-tenancy and server deployments](./multi-tenancy.md): SDK options for multi-user server mode * [GitHub OAuth](./github-oauth.md): implement the OAuth flow * [Azure managed identity](./azure-managed-identity.md): BYOK with Azure AI Foundry * [Scaling and multi-tenancy](./scaling.md): horizontal scaling, isolation patterns diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md new file mode 100644 index 000000000..82c4bb007 --- /dev/null +++ b/docs/setup/multi-tenancy.md @@ -0,0 +1,357 @@ +# Multi-tenancy and server deployments + +Multi-user server mode means running the Copilot SDK from backend code that serves more than one human, tenant, workspace, or integration account. In this setup, the application owns request routing and authorization, while the SDK and runtime provide per-session state, per-session authentication, and explicit tool registration so one user's session does not inherit another user's tools or identity. + +**Best for:** SaaS products, partner integrations, internal platforms, and backend services that handle concurrent users. + +## Use this guide when + +Use this guide when you are building: + +* A multi-user SaaS product that embeds Copilot-powered agents +* A backend for a partner integration, such as a Copilot Studio or Fabric-style pattern +* Any server that handles concurrent users, workspaces, tenants, or requests +* A shared runtime where multiple SDK clients connect to one Copilot runtime process + +This guide is a sister to [Scaling and multi-tenancy](./scaling.md). Use that guide for topology, load-balancing, and storage patterns. Use this guide for SDK-level options and runtime isolation choices. + +## Key SDK options + +| Option | Use it for | Notes | +|--------|------------|-------| +| `mode: "empty"` | Disabling ambient OS tools and CLI defaults | Required for multi-user or shared scenarios. | +| `sessionIdleTimeoutSeconds` | Cleaning idle sessions | Set a server-side timeout for long-running processes. | +| `baseDirectory` | Isolating `COPILOT_HOME` per runtime instance | Ignored when connecting to an existing runtime. | +| `sessionFs` | Routing session filesystem storage off local disk | Pair with per-session filesystem providers. | +| `RuntimeConnection.forUri(url)` | Sharing one already-running runtime | Language names vary; see samples below. | +| Per-session `gitHubToken` | Scoping auth to the requesting user | Prefer this over a single shared user token. | + +### `mode: "empty"` + +`mode: "empty"` disables optional Copilot CLI behavior by default. In multi-user server mode, this is the safe baseline because your application must explicitly decide which tools, MCP servers, skills, and workspace paths a session can access. + +Do not use the default `mode: "copilot-cli"` for shared servers. That mode is intended for CLI-like coding agents and can expose ambient host filesystem capabilities. + +
+TypeScript + +```typescript +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + mode: "empty", + baseDirectory: `/var/lib/my-app/copilot/${runtimeInstanceId}`, + sessionIdleTimeoutSeconds: 900, + connection: RuntimeConnection.forUri(process.env.COPILOT_RUNTIME_URL!), +}); + +const session = await client.createSession({ + sessionId: `user-${user.id}-${crypto.randomUUID()}`, + model: "gpt-4.1", + availableTools: ["custom:lookupOrder", "custom:createTicket"], + gitHubToken: user.githubToken, +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient, RuntimeConnection +from copilot.session import PermissionHandler + +client = CopilotClient( + mode="empty", + base_directory=f"/var/lib/my-app/copilot/{runtime_instance_id}", + session_idle_timeout_seconds=900, + connection=RuntimeConnection.for_uri(runtime_url), +) +await client.start() + +session = await client.create_session( + session_id=f"user-{user.id}-{request_id}", + model="gpt-4.1", + available_tools=["custom:lookupOrder", "custom:createTicket"], + github_token=user.github_token, + on_permission_request=PermissionHandler.approve_all, +) +``` + +
+ +
+Go + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + Mode: copilot.ModeEmpty, + BaseDirectory: fmt.Sprintf("/var/lib/my-app/copilot/%s", runtimeInstanceID), + SessionIdleTimeoutSeconds: 900, + Connection: copilot.UriConnection{URL: runtimeURL}, +}) + +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: fmt.Sprintf("user-%s-%s", user.ID, requestID), + Model: "gpt-4.1", + AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"}, + GitHubToken: user.GitHubToken, +}) +``` + +
+ +
+.NET + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + Mode = CopilotClientMode.Empty, + BaseDirectory = $"/var/lib/my-app/copilot/{runtimeInstanceId}", + SessionIdleTimeoutSeconds = 900, + Connection = RuntimeConnection.ForUri(runtimeUrl), +}); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + SessionId = $"user-{user.Id}-{requestId}", + Model = "gpt-4.1", + AvailableTools = ["custom:lookupOrder", "custom:createTicket"], + GitHubToken = user.GitHubToken, +}); +``` + +
+ +
+Java + +```java +import java.util.List; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; + +var client = new CopilotClient(new CopilotClientOptions() + .setMode(CopilotClientMode.EMPTY) + .setCopilotHome("/var/lib/my-app/copilot/" + runtimeInstanceId) + .setSessionIdleTimeoutSeconds(900) + .setCliUrl(runtimeUrl) +); + +var session = client.createSession(new SessionConfig() + .setSessionId("user-" + user.id() + "-" + requestId) + .setModel("gpt-4.1") + .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket")) + .setGitHubToken(user.gitHubToken()) +).get(); +``` + +
+ +
+Rust + +```rust +use std::path::PathBuf; +use github_copilot_sdk::{Client, ClientOptions, Transport}; +use github_copilot_sdk::mode::ClientMode; +use github_copilot_sdk::types::SessionConfig; + +let client = Client::start( + ClientOptions::new() + .with_mode(ClientMode::Empty) + .with_base_directory(PathBuf::from(format!( + "/var/lib/my-app/copilot/{runtime_instance_id}" + ))) + .with_session_idle_timeout_seconds(900) + .with_transport(Transport::External { + host: runtime_host.to_string(), + port: runtime_port, + connection_token: None, + }), +).await?; + +let session = client.create_session( + SessionConfig::default() + .with_session_id(format!("user-{}-{request_id}", user.id)) + .with_model("gpt-4.1") + .with_available_tools(["custom:lookupOrder", "custom:createTicket"]) + .with_github_token(user.github_token), +).await?; +``` + +
+ +### `sessionIdleTimeoutSeconds` + +Set `sessionIdleTimeoutSeconds` on servers so inactive sessions are cleaned up automatically. This prevents zombie sessions in long-running processes and reduces memory and filesystem pressure. + +| Language | Public option | +|----------|---------------| +| TypeScript | `sessionIdleTimeoutSeconds` | +| Python | `session_idle_timeout_seconds` | +| Go | `SessionIdleTimeoutSeconds` | +| .NET | `SessionIdleTimeoutSeconds` | +| Java | `setSessionIdleTimeoutSeconds(...)` | +| Rust | `with_session_idle_timeout_seconds(...)` | + +Use a value that matches your product's conversation lifetime. For chat backends, 15 to 30 minutes is usually a good starting point. For workflow agents, use a longer timeout and explicit deletion when the workflow completes. + +### `baseDirectory` + +`baseDirectory` sets `COPILOT_HOME` for a runtime instance. Use it to isolate runtime state, credentials, and session data per process, pod, worker, or tenant boundary. + +```typescript +const client = new CopilotClient({ + mode: "empty", + baseDirectory: `/var/lib/my-app/copilot/runtime-${process.env.HOSTNAME}`, + sessionIdleTimeoutSeconds: 900, +}); +``` + +The runtime stores session state under the configured `COPILOT_HOME`, including `session-state/{sessionId}`. If your app runs multiple runtime instances, give each instance a distinct directory unless you intentionally use shared storage. + +When the SDK connects to an already-running runtime with `RuntimeConnection.forUri(url)`, `baseDirectory` is ignored by the SDK client. Configure `COPILOT_HOME` on the runtime process instead. + +### `sessionFs` + +`sessionFs` registers a custom session filesystem provider so session-scoped file I/O can be routed through application storage instead of the runtime's local disk. Use it when local disk is ephemeral, when session state needs to live in object storage, or when a platform needs to enforce tenant-aware storage paths. + +```typescript +const client = new CopilotClient({ + mode: "empty", + sessionFs: { + initialWorkingDirectory: "/workspace", + sessionStatePath: "/session-state", + conventions: "posix", + }, +}); +``` + +For languages that expose a provider callback, configure `sessionFs` at the client level and provide a per-session filesystem handler when creating or resuming a session. See [Session Persistence](../features/session-persistence.md) for persistence concepts and storage trade-offs. + +Verified public SDK surfaces: + +| Language | Client-level config | Per-session provider | +|----------|---------------------|----------------------| +| TypeScript | `sessionFs` | `createSessionFsAdapter` / provider callbacks | +| Python | `session_fs` | `create_session_fs_handler` | +| Go | `SessionFs` | `CreateSessionFsProvider` | +| .NET | `SessionFs` | `CreateSessionFsProvider` | +| Rust | `with_session_fs(...)` | `with_session_fs_provider(...)` | + +Java does not currently expose a verified public `sessionFs` option, so this guide does not show a Java `sessionFs` sample. + +### `RuntimeConnection.forUri(url)` + +Use an external runtime connection when multiple SDK clients should share one already-running runtime. This is common in backend services where the runtime process is managed separately from request handlers. + +| Language | External runtime connection | +|----------|-----------------------------| +| TypeScript | `RuntimeConnection.forUri(url)` or `cliUrl` | +| Python | `RuntimeConnection.for_uri(url)` | +| Go | `copilot.UriConnection{URL: url}` | +| .NET | `RuntimeConnection.ForUri(url)` | +| Java | `setCliUrl(url)` | +| Rust | `Transport::External { host, port, connection_token }` | + +External runtimes manage their own process-level authentication and storage. Pass per-session tokens on `createSession` or `resumeSession` when you need user-specific auth. + +### Per-session `gitHubToken` + +Set `gitHubToken` on each session to scope GitHub auth to the requesting user. This is different from a client-level token, which authenticates the runtime process. + +```typescript +const session = await client.createSession({ + sessionId: `user-${user.id}-support`, + model: "gpt-4.1", + availableTools: ["custom:*"], + gitHubToken: user.githubToken, +}); +``` + +Use per-session tokens for content exclusion, model routing, quota checks, and user-specific Copilot access. Avoid sharing one service token across users unless your product intentionally uses service-account semantics. + +## Integration ID + +Partners building branded agents can set an integration ID for Mission Control requests. The runtime reads `GITHUB_COPILOT_INTEGRATION_ID` and stamps it as the `Copilot-Integration-Id` HTTP header on every Mission Control request. + +```bash +GITHUB_COPILOT_INTEGRATION_ID=my-product-agent copilot --headless --port 4321 +``` + +The default integration ID is `copilot-developer-cli`. Use a stable value such as `my-product-agent` for attribution and routing. The integration ID is currently configured by environment variable only; it is not a first-class SDK option. + +If the SDK spawns the runtime, pass the environment variable through the client environment option. If you connect with `RuntimeConnection.forUri(url)`, set the environment variable on the runtime process itself. + +## Session-level isolation guarantees + +Session-level isolation means the runtime keeps user-specific model and state information scoped to a session, not in global shared state. + +| Surface | Isolation behavior | +|---------|--------------------| +| Model list cache | Per-session. Model lookup uses the session's model list cache. | +| Session state | Per session ID under `COPILOT_HOME/session-state/{sessionId}`. | +| GitHub identity | Per-session when `gitHubToken` is set on the session. | +| Tools | Explicit in `mode: "empty"`; ambient in `mode: "copilot-cli"`. | +| Host filesystem | Shared by the runtime process if host tools are available. | + +`mode: "empty"` is what makes shared runtime patterns viable: no ambient OS tools are exposed unless your application registers or allows them. With `mode: "copilot-cli"`, OS filesystem access is shared through the host process, so do not use that mode for multi-user server mode. + +Session state is stored under `COPILOT_HOME/session-state/{sessionId}` unless you route it through `sessionFs`. Use unique session IDs that include your own tenant or user boundary, and enforce access control before resuming or deleting sessions. + +## Pattern comparison + +| Pattern | Use when | Trade-offs | +|---------|----------|------------| +| Pattern 1: isolated CLI per user | You need the strongest isolation boundary or separate process credentials per user. | Strong isolation; higher resource cost. See [Scaling and multi-tenancy](./scaling.md). | +| Pattern 2: shared CLI with `mode: "empty"` | You want one runtime to serve many users while your app controls tools, auth, and session IDs. | Efficient; requires careful tool registration, per-session tokens, and application-level access checks. | +| Pattern 3: hybrid | You route compute-heavy work to cloud sessions and light work to local sessions. | Flexible; requires workload routing and policy handling. See [Cloud Sessions](../features/cloud-sessions.md). | + +### Pattern 2: shared CLI with `mode: "empty"` + +In this pattern, all users connect through your backend to one runtime pool. The application performs user authentication, chooses a session ID, passes the user's GitHub token on the session, and provides an explicit tool allowlist. + +```mermaid +flowchart TB + U1["User A"] --> API["Your backend"] + U2["User B"] --> API + API --> Runtime["Shared Copilot runtime"] + Runtime --> SA["session-state/user-a-..."] + Runtime --> SB["session-state/user-b-..."] + + API -. "mode: empty" .-> Runtime + API -. "per-session gitHubToken" .-> Runtime + + style API fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 + style Runtime fill:#0d1117,stroke:#3fb950,color:#c9d1d9 +``` + +Use these rules: + +* Always start the client or runtime in `mode: "empty"`. +* Use unique session IDs and store ownership metadata in your application database. +* Check ownership before `resumeSession`, `deleteSession`, or any UI action that references a session ID. +* Pass `gitHubToken` per session when requests should run as the user. +* Register only the tools the session needs, and prefer source-qualified allowlists such as `custom:*` or `mcp:search_docs`. +* Set `sessionIdleTimeoutSeconds` and delete completed workflow sessions explicitly. + +## Common pitfalls + +* Forgetting `mode: "empty"`. The default `copilot-cli` mode exposes CLI-style behavior and may expose the host filesystem through ambient tools. +* Not setting `sessionIdleTimeoutSeconds`. Long-running servers can accumulate idle sessions if they do not clean them up. +* Sharing one `gitHubToken` across users instead of passing a per-session token. +* Trusting client-provided session IDs without checking ownership in your backend. +* Setting `baseDirectory` on a client that connects to an existing runtime and expecting it to move runtime storage. Configure the runtime process instead. +* Allowing broad tool patterns such as `builtin:*` without reviewing whether each tool is appropriate for your users. + +## See also + +* [Scaling and multi-tenancy](./scaling.md): deployment topologies, storage patterns, and isolation comparisons +* [Backend services setup](./backend-services.md): running the runtime in headless server mode +* [BYOK](../auth/byok.md): using your own model provider credentials +* [Cloud Sessions](../features/cloud-sessions.md): routing selected work to cloud sessions +* [Session Persistence](../features/session-persistence.md): managing resumable session state +* [Features overview](../features/index.md): tools, events, hooks, and advanced SDK features diff --git a/docs/setup/scaling.md b/docs/setup/scaling.md index 371a402b3..d960eb94e 100644 --- a/docs/setup/scaling.md +++ b/docs/setup/scaling.md @@ -2,6 +2,8 @@ Design your Copilot SDK deployment to serve multiple users, handle concurrent sessions, and scale horizontally across infrastructure. This guide covers session isolation patterns, scaling topologies, and production best practices. +For SDK-level options and patterns, see [Multi-Tenancy & Server Deployments](./multi-tenancy.md). + **Best for:** Platform developers, SaaS builders, any deployment serving more than a handful of concurrent users. ## Core concepts diff --git a/docs/troubleshooting/compatibility.md b/docs/troubleshooting/compatibility.md index 89476b26f..c68d59cc7 100644 --- a/docs/troubleshooting/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -86,7 +86,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Working directory | `workingDirectory` config | Set session cwd | | **Experimental** | | | | Agent management | `session.rpc.agent.*` | List, select, deselect, get current agent | -| Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution | +| Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution; see [Fleet mode](../features/fleet-mode.md) | | Manual compaction | `session.rpc.history.compact()` | Trigger compaction on demand | | History truncation | `session.rpc.history.truncate()` | Remove events from a point onward | | Session forking | `server.rpc.sessions.fork()` | Fork a session at a point in history | @@ -170,6 +170,10 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b ## Workarounds +### Fleet mode + +Fleet mode is available through `session.rpc.fleet.start()` for SDK applications that want the runtime to dispatch parallel sub-agents for a larger objective. Use it when independent subtasks can run concurrently and then be summarized by the main session. For a full guide, see [Fleet mode](../features/fleet-mode.md). + ### Session export The `--share` option is not available via SDK. Workarounds: diff --git a/docs/troubleshooting/mcp-debugging.md b/docs/troubleshooting/mcp-debugging.md index 664826c6e..3447ed921 100644 --- a/docs/troubleshooting/mcp-debugging.md +++ b/docs/troubleshooting/mcp-debugging.md @@ -393,7 +393,7 @@ Create a wrapper script to log all communication: #!/bin/bash # mcp-debug-wrapper.sh -LOG="/tmp/mcp-debug-$(date +%s).log" +LOG="./mcp-debug-$(date +%s).log" ACTUAL_SERVER="$1" shift diff --git a/dotnet/README.md b/dotnet/README.md index 719c554f4..9b266421f 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -2,12 +2,10 @@ SDK for programmatic control of GitHub Copilot CLI. -> **Note:** This SDK is in public preview and may change in breaking ways. - ## Installation ```bash -dotnet add package GitHub.Copilot +dotnet add package GitHub.Copilot.SDK ``` ## Run the Samples diff --git a/go/README.md b/go/README.md index 568d75f9d..31aef65e3 100644 --- a/go/README.md +++ b/go/README.md @@ -2,8 +2,6 @@ A Go SDK for programmatic access to the GitHub Copilot CLI. -> **Note:** This SDK is in public preview and may change in breaking ways. - ## Installation ```bash @@ -104,13 +102,13 @@ That's it! When your application calls `copilot.NewClient` without a `Connection - `Start(ctx context.Context) error` - Start the CLI server - `Stop() error` - Stop the CLI server - `ForceStop()` - Forcefully stop without graceful cleanup -- `CreateSession(config *SessionConfig) (*Session, error)` - Create a new session -- `ResumeSession(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume an existing session -- `ResumeSessionWithOptions(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration -- `ListSessions(filter *SessionListFilter) ([]SessionMetadata, error)` - List sessions (with optional filter) -- `DeleteSession(sessionID string) error` - Delete a session permanently +- `CreateSession(ctx context.Context, config *SessionConfig) (*Session, error)` - Create a new session +- `ResumeSession(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume an existing session +- `ResumeSessionWithOptions(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration +- `ListSessions(ctx context.Context, filter *SessionListFilter) ([]SessionMetadata, error)` - List sessions (with optional filter) +- `DeleteSession(ctx context.Context, sessionID string) error` - Delete a session permanently - `GetLastSessionID(ctx context.Context) (*string, error)` - Get the ID of the most recently updated session -- `Ping(message string) (*PingResponse, error)` - Ping the server +- `Ping(ctx context.Context, message string) (*PingResponse, error)` - Ping the server - `RuntimePort() int` - TCP port the runtime is listening on (0 if stdio) - `GetForegroundSessionID(ctx context.Context) (*string, error)` - Get the session ID currently displayed in TUI (TUI+server mode only) - `SetForegroundSessionID(ctx context.Context, sessionID string) error` - Request TUI to display a specific session (TUI+server mode only) diff --git a/java/README.md b/java/README.md index 61e59c8d6..aba69df42 100644 --- a/java/README.md +++ b/java/README.md @@ -13,24 +13,24 @@ ## Background -> ℹ️ **Public Preview:** This SDK tracks the [GitHub Copilot SDKs](https://github.com/github/copilot-sdk) for [.NET](https://github.com/github/copilot-sdk/tree/main/dotnet) and [Node.js](https://github.com/github/copilot-sdk/tree/main/nodejs). While in public preview, minor breaking changes may still occur between releases. - -Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. +Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. The Java SDK tracks the official GitHub Copilot SDK family (TypeScript, Python, Go, .NET, and Rust). ## Installation ### Requirements -- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start). +- Java 17 or later. **JDK 21 or later recommended** for virtual thread support, as shown in the [Quick Start](#quick-start). - GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`) ### Maven +Replace `${copilot.sdk.version}` with the latest release from Maven Central. + ```xml com.github copilot-sdk-java - 1.0.0-beta-java.4 + ${copilot.sdk.version} ``` @@ -56,8 +56,10 @@ Snapshot builds of the next development version are published to Maven Central S ### Gradle +Replace `${copilot.sdk.version}` with the latest release from Maven Central. + ```groovy -implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.4' +implementation 'com.github:copilot-sdk-java:${copilot.sdk.version}' ``` ## Quick Start @@ -78,8 +80,8 @@ public class CopilotSDK { var lastMessage = new String[]{null}; // Create and start client - try (var client = new CopilotClient()) { // JDK 25+: comment out this line - // JDK 25+: uncomment the following 3 lines for virtual thread support + try (var client = new CopilotClient()) { // JDK 21+: comment out this line + // JDK 21+: uncomment the following 3 lines for virtual thread support // var options = new CopilotClientOptions() // .setExecutor(Executors.newVirtualThreadPerTaskExecutor()); // try (var client = new CopilotClient(options)) { diff --git a/java/docs/adr/adr-001-semver-pre-general-availability.md b/java/docs/adr/adr-001-semver-pre-general-availability.md index 25008b0f5..b081e1fe3 100644 --- a/java/docs/adr/adr-001-semver-pre-general-availability.md +++ b/java/docs/adr/adr-001-semver-pre-general-availability.md @@ -1,3 +1,5 @@ +Status: This ADR's pre-general-availability SemVer policy is superseded by the generally available release; see CHANGELOG and the README for the current SemVer policy. + # SemVer requirements pre general-availability of Reference Implementation ## Context and Problem Statement diff --git a/nodejs/README.md b/nodejs/README.md index aadf7c677..d7e60b667 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -2,8 +2,6 @@ TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. -> **Note:** This SDK is in public preview and may change in breaking ways. - ## Installation ```bash @@ -57,7 +55,7 @@ await session.disconnect(); await client.stop(); ``` -Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+/Node.js 18.0+): +Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+ / Node.js 20+): ```typescript await using session = await client.createSession({ @@ -82,14 +80,20 @@ new CopilotClient(options?: CopilotClientOptions) - `connection?: RuntimeConnection` - How to connect to the Copilot runtime. Construct via the factory functions on `RuntimeConnection`: - `RuntimeConnection.forStdio({ path?, args? })` (default) — spawn the runtime and communicate over its stdin/stdout. - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })` — spawn the runtime as a TCP server. - - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). -- `cwd?: string` - Working directory for the runtime process (default: current process cwd). + - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections. +- `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`. +- `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd). - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. -- `logLevel?: string` - Log level. When omitted, the runtime uses its own default (currently `"info"`). +- `logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"` - Log level. When omitted, the runtime uses its own default (currently `"info"`). +- `env?: Record` - Environment variables for the runtime process. When omitted, inherits `process.env`. - `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. - `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `gitHubToken` is provided). Cannot be used with `RuntimeConnection.forUri`. +- `onListModels?: () => Promise | ModelInfo[]` - Optional model-list provider, useful when using a custom provider. - `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the runtime process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. - `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the runtime's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. +- `sessionFs?: SessionFsConfig` - Custom session filesystem provider. +- `sessionIdleTimeoutSeconds?: number` - Server-wide idle timeout for sessions in seconds. Ignored when connecting via `RuntimeConnection.forUri`. +- `enableRemoteSessions?: boolean` - Enable Mission Control remote session support. Ignored when connecting via `RuntimeConnection.forUri`. #### Methods @@ -163,7 +167,7 @@ Get the ID of the session currently displayed in the TUI. Only available when co Request the TUI to switch to displaying the specified session. Only available in TUI+server mode. -##### `on(eventType: SessionLifecycleEventType, handler): () => void` +##### `onLifecycle(eventType: SessionLifecycleEventType, handler): () => void` Subscribe to a specific session lifecycle event type. Returns an unsubscribe function. @@ -173,7 +177,7 @@ const unsubscribe = client.onLifecycle("session.foreground", (event) => { }); ``` -##### `on(handler: SessionLifecycleHandler): () => void` +##### `onLifecycle(handler: SessionLifecycleHandler): () => void` Subscribe to all session lifecycle events. Returns an unsubscribe function. diff --git a/nodejs/docs/examples.md b/nodejs/docs/examples.md index a2b106a48..1bac87982 100644 --- a/nodejs/docs/examples.md +++ b/nodejs/docs/examples.md @@ -158,7 +158,7 @@ Hooks intercept and modify behavior at key lifecycle points. Register them in th | `onPreToolUse` | Before a tool executes | Tool args, permission decision, add context | | `onPostToolUse` | After a tool executes successfully | Tool result, add context | | `onPostToolUseFailure` | After a tool execution returns a failure | Add hidden guidance to the model | -| `onSessionStart` | Session starts or resumes | Add context, modify config | +| `onSessionStart` | Session starts or resumes | Add context | | `onSessionEnd` | Session ends | Cleanup actions, summary | | `onErrorOccurred` | An error occurs | Error handling strategy (retry/skip/abort) | @@ -415,7 +415,7 @@ session.on("assistant.message", (event) => { | Event Type | Description | Key Data Fields | | --------------------------- | ------------------------------------------------ | ------------------------------------------------------ | | `assistant.message` | Agent's final response | `content`, `messageId`, `toolRequests` | -| `assistant.streaming_delta` | Token-by-token streaming (ephemeral) | `totalResponseSizeBytes` | +| `assistant.message_delta` | Message content chunks (ephemeral) | `deltaContent` | | `tool.execution_start` | A tool is about to run | `toolCallId`, `toolName`, `arguments` | | `tool.execution_complete` | A tool finished running | `toolCallId`, `toolName`, `success`, `result`, `error` | | `user.message` | User sent a message | `content`, `attachments`, `source` | @@ -629,8 +629,11 @@ const session = await joinSession({ onPreToolUse: async (input) => { if (input.toolName === "bash") { const cmd = String(input.toolArgs?.command || ""); - if (/rm\\s+-rf\\s+\\/ / i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) { - return { permissionDecision: "deny" }; + if (/rm\\s+-rf\\s+\//i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) { + return { + permissionDecision: "deny", + permissionDecisionReason: "Destructive commands are not allowed.", + }; } } }, diff --git a/python/README.md b/python/README.md index 6445ed1e9..3a43adae1 100644 --- a/python/README.md +++ b/python/README.md @@ -2,14 +2,16 @@ Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. -> **Note:** This SDK is in public preview and may change in breaking ways. - ## Installation ```bash -pip install -e ".[telemetry,dev]" -# or -uv pip install -e ".[telemetry,dev]" +pip install github-copilot-sdk +``` + +To include OpenTelemetry support: + +```bash +pip install "github-copilot-sdk[telemetry]" ``` ## Run the Sample @@ -158,8 +160,11 @@ All options are kw-only parameters: - `base_directory` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using a `UriRuntimeConnection`. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). - `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `session_fs` (dict | None): Connection-level session filesystem provider configuration. +- `session_idle_timeout_seconds` (int | None): Server-wide session idle timeout in seconds. Set to `None` or `0` to disable. - `enable_remote_sessions` (bool): Enable remote/cloud session support (default: False). - `on_list_models` (callable | None): Custom handler for `list_models()`. When provided, the handler is called instead of querying the runtime. +- `mode` (str): Client mode (default: `"copilot-cli"`). **RuntimeConnection variants:** @@ -549,7 +554,7 @@ client = CopilotClient( Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `create_session`, `resume_session`, and `send` calls, and inbound when the CLI invokes tool handlers. -Install with telemetry extras: `pip install copilot-sdk[telemetry]` (provides `opentelemetry-api`) +Install with telemetry extras: `pip install "github-copilot-sdk[telemetry]"` (provides `opentelemetry-api`) ## Permission Handling diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 44c4b369e..d835ed276 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,7 +3,7 @@ name = "github-copilot-sdk" version = "0.0.0-dev" edition = "2024" rust-version = "1.94.0" -description = "Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC. Technical preview, pre-1.0." +description = "Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC." keywords = ["copilot", "github", "ai", "json-rpc", "sdk"] categories = ["api-bindings", "development-tools"] repository = "https://github.com/github/copilot-sdk" diff --git a/rust/README.md b/rust/README.md index 215448e05..ca1fa7cf7 100644 --- a/rust/README.md +++ b/rust/README.md @@ -2,9 +2,7 @@ A Rust SDK for programmatic access to the GitHub Copilot CLI. -> **Note:** This SDK is in technical preview and may change in breaking ways. - -See [github/copilot-sdk](https://github.com/github/copilot-sdk) for the equivalent SDKs in TypeScript, Python, Go, and .NET. The Rust SDK seeks parity with those SDKs; see [Differences From Other SDKs](#differences-from-other-sdks) below for the small set of intentional divergences. +See [github/copilot-sdk](https://github.com/github/copilot-sdk) for the equivalent SDKs in TypeScript, Python, Go, .NET, and Java. The Rust SDK seeks parity with those SDKs; see [Differences From Other SDKs](#differences-from-other-sdks) below for the small set of intentional divergences. **Releases:** [github.com/github/copilot-sdk/releases?q=rust%2F](https://github.com/github/copilot-sdk/releases?q=rust%2F) — per-version release notes for the Rust crate. From 359183b74d7ddd1299c2d7704672417d6b0ef9e4 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 May 2026 09:05:46 -0700 Subject: [PATCH 2/8] docs(cloud-sessions): remove confusing remote-sessions/sandbox disambiguation intro Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/cloud-sessions.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md index d87cf0e3b..d9a7d66bd 100644 --- a/docs/features/cloud-sessions.md +++ b/docs/features/cloud-sessions.md @@ -2,13 +2,6 @@ Cloud sessions run Copilot work on GitHub-hosted compute through Mission Control. Use them when your app should create a session that executes remotely instead of starting a local Copilot CLI session on the user's machine or your server. -Cloud sessions are distinct from [remote sessions](./remote-sessions.md). Remote sessions are locally hosted sessions that are also surfaced through Mission Control so users can view and steer them from GitHub web and mobile. Cloud sessions are created with the `cloud` create-session option and are routed by Mission Control to GitHub-hosted compute. - -Cloud sessions are also distinct from the Windows sandbox. The Windows sandbox is local AppContainer tool isolation and does not create GitHub-hosted compute. - -> [!NOTE] -> Don't confuse cloud sessions with the Windows sandbox. The Windows sandbox is a local AppContainer for tool isolation, enabled with `SANDBOX=true`. They are unrelated features. - ## Prerequisites Before creating a cloud session, make sure: From 3d5606ea6cd52b23ede2c262261205cf36d65d46 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 May 2026 09:23:36 -0700 Subject: [PATCH 3/8] docs: validate new GA samples instead of using docs-validate: skip Replace all 9 docs-validate: skip markers in cloud-sessions.md with the hidden-full-snippet + visible-snippet pattern (or direct buildable snippets), and fix latent validation failures in fleet-mode.md and multi-tenancy.md (.NET undefined locals, Go missing package/imports, TS plan-mode union literal). - cloud-sessions.md: 0 skip markers remain - fleet-mode.md: hidden Go/.NET wrappers for session+ctx; plan-mode snippet now a valid type union - multi-tenancy.md: hidden Go/.NET wrappers seed runtimeInstanceId, runtimeUrl, user, requestId Validation (TS / Python / Go / C#) passes for all three files; Java validation requires mvn (only available in CI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/cloud-sessions.md | 103 ++++++++++++++++++++++++++++---- docs/features/fleet-mode.md | 59 +++++++++++++++++- docs/setup/multi-tenancy.md | 69 +++++++++++++++++++++ 3 files changed, 217 insertions(+), 14 deletions(-) diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md index d9a7d66bd..684486c06 100644 --- a/docs/features/cloud-sessions.md +++ b/docs/features/cloud-sessions.md @@ -19,7 +19,6 @@ Set the create-session `cloud` option to create a cloud session. You can include ### TypeScript - ```typescript import { CopilotClient } from "@github/copilot-sdk"; @@ -40,10 +39,13 @@ const session = await client.createSession({ ### Python - ```python -from copilot import CopilotClient, CloudSessionOptions, CloudSessionRepository -from copilot.session import PermissionHandler +from copilot import ( + CloudSessionOptions, + CloudSessionRepository, + CopilotClient, + PermissionHandler, +) client = CopilotClient() await client.start() @@ -62,7 +64,45 @@ session = await client.create_session( ### Go - + +```go +package main + +import ( + "context" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + +func main() { + _ = run(context.Background()) +} + +func run(ctx context.Context) error { + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + return err + } + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Cloud: &copilot.CloudSessionOptions{ + Repository: &copilot.CloudSessionRepository{ + Owner: "github", + Name: "copilot-sdk", + Branch: "main", + }, + }, + OnPermissionRequest: func(_ copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil + }, + }) + _ = session + return err +} +``` + + ```go client := copilot.NewClient(nil) if err := client.Start(ctx); err != nil { @@ -86,7 +126,6 @@ _ = session ### .NET - ```csharp await using var client = new CopilotClient(); @@ -108,7 +147,6 @@ var session = await client.CreateSessionAsync(new SessionConfig ### Java - ```java import com.github.copilot.CopilotClient; import com.github.copilot.rpc.*; @@ -130,7 +168,6 @@ try (var client = new CopilotClient()) { ### Rust - ```rust use github_copilot_sdk::{CloudSessionOptions, CloudSessionRepository, SessionConfig}; use github_copilot_sdk::handler::PermissionResult; @@ -166,9 +203,24 @@ Use `branch` when the work should start from a specific branch. If your app is c The `cloud` option only applies when creating a new session. To resume an existing cloud session, use the standard resume API for the SDK language: - + ```typescript -const session = await client.resumeSession("session-id"); +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.resumeSession("session-id", { + onPermissionRequest: async () => ({ kind: "approve-once" }), +}); +void session; +``` + + +```typescript +const session = await client.resumeSession("session-id", { + onPermissionRequest: async () => ({ kind: "approve-once" }), +}); ``` Do not pass `cloud` again on resume. The saved session metadata determines that the session is cloud-backed, and resume follows the normal session resume path. @@ -181,7 +233,35 @@ When this happens, the runtime reports a `"policy_blocked"` failure reason for c In TypeScript, check for the reason before retrying: - + +```typescript +import { + CopilotClient, + type CloudSessionRepository, +} from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const repository: CloudSessionRepository = { + owner: "github", + name: "copilot-sdk", +}; + +try { + await client.createSession({ + cloud: { repository }, + onPermissionRequest: async () => ({ kind: "approve-once" }), + }); +} catch (error) { + if ((error as { reason?: string }).reason === "policy_blocked") { + // Show an admin-facing message or link to org policy settings. + } + throw error; +} +``` + + ```typescript try { await client.createSession({ cloud: { repository } }); @@ -209,7 +289,6 @@ By default, the runtime derives the Mission Control base URL from the configured This may be required for GitHub Enterprise Server deployments. Confirm the correct value and support status with your GitHub representative before relying on it in production. - ```shell COPILOT_MC_BASE_URL="https://example.com/agents" ``` diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index 51732ce7d..2de9e8806 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -67,6 +67,40 @@ if result.started:
Go + +```go +package main + +import ( + "context" + "fmt" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{}) + if err != nil { + return + } + + prompt := "Update each package independently, then report validation results." + result, err := session.RPC.Fleet.Start(ctx, &rpc.FleetStartRequest{ + Prompt: &prompt, + }) + if err != nil { + return + } + if result.Started { + fmt.Println("Fleet mode started") + } +} +``` + + ```go prompt := "Update each package independently, then report validation results." result, err := session.RPC.Fleet.Start(ctx, &rpc.FleetStartRequest{ @@ -85,6 +119,23 @@ if result.Started {
.NET + +```csharp +using GitHub.Copilot; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig()); + +var result = await session.Rpc.Fleet.StartAsync( + "Audit each project independently, then summarize the findings."); + +if (result.Started) +{ + Console.WriteLine("Fleet mode started"); +} +``` + + ```csharp var result = await session.Rpc.Fleet.StartAsync( "Audit each project independently, then summarize the findings."); @@ -125,8 +176,12 @@ Native typed bindings for fleet mode were verified in Node.js/TypeScript, Python Plan-mode UIs can start fleet deployment by returning the `autopilot_fleet` exit action. The generated session event types describe it as: ```typescript -/** Exit plan mode and continue with parallel autonomous workers. */ -| "autopilot_fleet"; +type PlanModeExitAction = + | "exit_only" + | "interactive" + | "autopilot" + /** Exit plan mode and continue with parallel autonomous workers. */ + | "autopilot_fleet"; ``` Use this when a user approves a plan that already contains independent work items. Use `autopilot` for a single autonomous worker and `interactive` when the user should stay in the loop. diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md index 82c4bb007..3c84110ce 100644 --- a/docs/setup/multi-tenancy.md +++ b/docs/setup/multi-tenancy.md @@ -84,6 +84,48 @@ session = await client.create_session(
Go + +```go +package main + +import ( + "context" + "fmt" + + copilot "github.com/github/copilot-sdk/go" +) + +type appUser struct { + ID string + GitHubToken string +} + +func main() { + ctx := context.Background() + runtimeInstanceID := "instance-1" + runtimeURL := "http://127.0.0.1:8080" + requestID := "req-1" + user := appUser{ID: "alice", GitHubToken: "gho_xxx"} + + client := copilot.NewClient(&copilot.ClientOptions{ + Mode: copilot.ModeEmpty, + BaseDirectory: fmt.Sprintf("/var/lib/my-app/copilot/%s", runtimeInstanceID), + SessionIdleTimeoutSeconds: 900, + Connection: copilot.UriConnection{URL: runtimeURL}, + }) + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: fmt.Sprintf("user-%s-%s", user.ID, requestID), + Model: "gpt-4.1", + AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"}, + GitHubToken: user.GitHubToken, + }) + _ = session + _ = err +} +``` + + ```go client := copilot.NewClient(&copilot.ClientOptions{ Mode: copilot.ModeEmpty, @@ -105,6 +147,33 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
.NET + +```csharp +using GitHub.Copilot; + +var runtimeInstanceId = "instance-1"; +var runtimeUrl = "http://127.0.0.1:8080"; +var requestId = "req-1"; +var user = new { Id = "alice", GitHubToken = "gho_xxx" }; + +var client = new CopilotClient(new CopilotClientOptions +{ + Mode = CopilotClientMode.Empty, + BaseDirectory = $"/var/lib/my-app/copilot/{runtimeInstanceId}", + SessionIdleTimeoutSeconds = 900, + Connection = RuntimeConnection.ForUri(runtimeUrl), +}); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + SessionId = $"user-{user.Id}-{requestId}", + Model = "gpt-4.1", + AvailableTools = ["custom:lookupOrder", "custom:createTicket"], + GitHubToken = user.GitHubToken, +}); +``` + + ```csharp var client = new CopilotClient(new CopilotClientOptions { From e9026cdacc4826bf00d65ca7485dd8f3a2c80dbe Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 May 2026 09:27:49 -0700 Subject: [PATCH 4/8] docs: add plugin directories guide New guide covering the --plugin-dir / extra-args path for loading bundled plugin folders (skills, hooks, MCP, custom agents, LSP) from an SDK host application. Covers folder layout, per-language wiring, plugin-dir vs marketplace plugins, COPILOT_PLUGIN_DIR_ONLY for deterministic plugin sets, session.plugins.list inspection, and troubleshooting. Links added from docs/index.md and docs/features/index.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/index.md | 1 + docs/features/plugin-directories.md | 324 ++++++++++++++++++++++++++++ docs/index.md | 1 + 3 files changed, 326 insertions(+) create mode 100644 docs/features/plugin-directories.md diff --git a/docs/features/index.md b/docs/features/index.md index 6ea21e364..d6a560fc1 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -14,6 +14,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [Fleet Mode](./fleet-mode.md) | Dispatch multiple sub-agents in parallel for large, independent workstreams | | [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access | | [Skills](./skills.md) | Load reusable prompt modules from directories | +| [Plugin Directories](./plugin-directories.md) | Bundle skills, hooks, MCP servers, and agents as a single loadable plugin | | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing | diff --git a/docs/features/plugin-directories.md b/docs/features/plugin-directories.md new file mode 100644 index 000000000..66dc4b8f5 --- /dev/null +++ b/docs/features/plugin-directories.md @@ -0,0 +1,324 @@ +# Plugin directories + +A **plugin** is a directory that bundles SDK extensions — skills, hooks, MCP servers, custom agents, and LSP configuration — behind a single manifest. Pointing the SDK at a plugin directory loads everything the plugin contributes, so you can ship reusable capability packs without writing per-extension wiring in every host application. + +This guide explains the plugin folder layout, how to load a plugin from a directory, when to use plugin directories vs. registering individual extensions, and how to make plugin sets deterministic. + +## When to use plugin directories + +Use a plugin directory when you want to: + +* **Distribute a bundle of capabilities** as one unit — e.g., a "TypeScript reviewer" pack with a skill, a `preToolUse` hook that enforces lint, and a custom agent that runs the reviewer. +* **Vendor capability packs into a repository** so every clone of the host application loads the same extensions deterministically. +* **Develop a plugin locally** before publishing it to a marketplace. +* **Override or extend** a marketplace-installed plugin with a local checkout for testing. + +If you only need to add a single MCP server, a single hook, or a single custom agent, you can register it inline via the SDK config (`mcpServers`, `hooks`, `customAgents`). Plugin directories are most useful once you have three or more related extensions that ship together. + +## Plugin folder layout + +The Copilot CLI scans each plugin directory for a `plugin.json` manifest or a root-level `SKILL.md`. A minimal plugin looks like this: + +``` +my-plugin/ +├── plugin.json # manifest (required unless using SKILL.md only) +├── SKILL.md # optional: top-level skill +├── hooks.json # optional: hooks config +├── .mcp.json # optional: MCP server config +├── agents/ # optional: custom agents (one .md file per agent) +│ └── code-reviewer.md +└── skills/ # optional: additional skills + └── lint-fix/ + └── SKILL.md +``` + +The manifest may also live at `.github/plugin.json` or `.github/plugin/plugin.json` so plugins can sit inside an existing repository without changing its root layout. Each subsystem (hooks, MCP, LSP, skills, agents) has its own loader and is optional — a plugin only needs the parts it contributes. + +For the full manifest schema, see the runtime documentation referenced from your CLI's `/plugin` slash command. + +## Loading a plugin directory from the SDK + +Plugin directories are loaded by passing `--plugin-dir ` to the Copilot CLI when the SDK spawns it. Each language exposes this through the runtime connection's extra-args option. The flag can be repeated to load multiple plugins. + +
+Node.js / TypeScript + + +```typescript +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; + +async function main() { + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ + args: [ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + ], + }), + }); + + await client.start(); +} + +main(); +``` + + +```typescript +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ + args: [ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + ], + }), +}); + +await client.start(); +``` + +
+ +
+Python + + +```python +from copilot import CopilotClient, StdioRuntimeConnection + +client = CopilotClient( + connection=StdioRuntimeConnection( + args=( + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + ), + ), +) +await client.start() +``` + +
+ +
+Go + + +```go +package main + +import ( + "context" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.StdioConnection{ + Args: []string{ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + }, + }, + }) + if err := client.Start(ctx); err != nil { + return + } +} +``` + + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.StdioConnection{ + Args: []string{ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + }, + }, +}) +if err := client.Start(ctx); err != nil { + return err +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot; + +await using var client = new CopilotClient(new CopilotClientOptions +{ + Connection = RuntimeConnection.ForStdio(args: new[] + { + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + }), +}); + +await client.StartAsync(); +``` + +
+ +
+Java + +```java +import com.github.copilot.CopilotClient; +import com.github.copilot.CopilotClientOptions; + +var options = new CopilotClientOptions() + .setCliArgs(new String[] { + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + }); + +var client = new CopilotClient(options); +client.start().get(); +``` + +
+ +
+Rust + + +```rust +use github_copilot_sdk::{Client, ClientOptions}; + +let client = Client::start( + ClientOptions::new().with_extra_args([ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + ]), +) +.await?; +``` + +
+ +> The example above uses an stdio runtime connection — the default when the SDK bundles the CLI. If you connect to an external runtime via a URL (`forUri` / `ForUri`), pass `--plugin-dir` to the long-running CLI server when you start it; the SDK does not forward `--plugin-dir` to runtimes it didn't spawn. + +## What a plugin can contribute + +Loading a plugin directory makes its extensions visible to every session created by the client. The runtime merges plugin-provided extensions with anything you register inline: + +| Plugin contributes | Visible to session as | +|---|---| +| Skills (`SKILL.md`, `skills/*/SKILL.md`) | Items in `session.skills.list()`; injectable by name | +| Custom agents (`agents/*.md`) | Dispatchable via the `task(agent_type=...)` tool | +| Hooks (`hooks.json`) | Fired alongside hooks registered via the SDK | +| MCP servers (`.mcp.json`) | Tools and resources reachable through `session.mcp.*` | +| LSP servers (`.lsp.json`) | Initialized via `session.lsp.initialize(...)` | + +Plugin agents are first-class sub-agents in [fleet mode](./fleet-mode.md): a parent agent can dispatch them by `agent_type`, and the runtime fires the `subagentStart` / `subagentStop` hooks for them like any other sub-agent. + +## Plugin-dir vs marketplace plugins + +The runtime has two ways to install plugins, and both end up looking the same to a session: + +* **Marketplace / direct-repo plugins** are installed persistently through the CLI's `/plugin` slash command or the underlying `installedPlugins` user setting. They are *ambient* — every session that runs against the same user config sees them, and they participate in plugin discovery rules. +* **`--plugin-dir` plugins** are *explicit and ephemeral* — they only apply to the CLI process you launched with that flag. They take precedence over ambient discovery and are de-duplicated against marketplace entries with the same cache path, so the same plugin won't load twice when both surfaces reference it. + +For SDK-driven applications, `--plugin-dir` is usually the right choice: it keeps the plugin set under your application's control instead of depending on per-machine user state. + +## Making plugin sets deterministic + +When the host machine may have other plugins installed (marketplace or personal), set `COPILOT_PLUGIN_DIR_ONLY=true` in the runtime's environment to suppress automatic plugin discovery. Only the directories you pass via `--plugin-dir` will load. + +
+Node.js / TypeScript + + +```typescript +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; + +async function main() { + process.env.COPILOT_PLUGIN_DIR_ONLY = "true"; + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ + args: ["--plugin-dir", "./plugins/code-reviewer"], + }), + }); + await client.start(); +} + +main(); +``` + + +```typescript +process.env.COPILOT_PLUGIN_DIR_ONLY = "true"; + +const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ + args: ["--plugin-dir", "./plugins/code-reviewer"], + }), +}); +await client.start(); +``` + +
+ +Use this in CI, in headless server deployments, and anywhere you want a reproducible plugin set that doesn't depend on the host's user configuration. + +## Inspecting which plugins loaded + +Once a session is created, list the active plugins to confirm a directory was picked up correctly: + +
+Node.js / TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +async function main() { + const client = new CopilotClient(); + await client.start(); + const session = await client.createSession({ + onPermissionRequest: async () => ({ kind: "approve-once" }), + }); + + const plugins = await session.rpc.plugins.list(); + for (const plugin of plugins.plugins) { + console.log(`${plugin.name} (${plugin.enabled ? "enabled" : "disabled"})`); + } +} + +main(); +``` + + +```typescript +const plugins = await session.rpc.plugins.list(); +for (const plugin of plugins.plugins) { + console.log(`${plugin.name} (${plugin.enabled ? "enabled" : "disabled"})`); +} +``` + +
+ +Plugins loaded via `--plugin-dir` appear in this list with their cache path set to the directory you provided. Marketplace installs are tagged with their registry source. + +## Troubleshooting + +* **"no plugin.json or SKILL.md found in <dir>"** — the directory exists but doesn't qualify as a plugin. Add a `plugin.json` manifest at the root (or under `.github/`), or include a top-level `SKILL.md`. +* **Plugin loaded but agents/skills not visible** — make sure the plugin manifest declares the agents/skills it contributes, or use the implicit layout (`agents/*.md`, `skills/*/SKILL.md`). Then call `session.rpc.skills.reload()` to pick up changes without restarting. +* **Duplicate hooks firing** — the runtime de-duplicates by `cache_path`, but only when the same directory is referenced both as a marketplace install and a `--plugin-dir`. If two different directories contain the same plugin, both will load. Remove one or use `COPILOT_PLUGIN_DIR_ONLY=true`. +* **`--plugin-dir` ignored when connecting to an external runtime** — the SDK only forwards extra args when it spawns the CLI itself. For external runtimes (`forUri`/`ForUri`), pass `--plugin-dir` on the command line that starts the runtime server. + +## Related + +* [Custom Agents](./custom-agents.md): write agents that ship inside a plugin's `agents/` folder. +* [Skills](./skills.md): how `SKILL.md` files are loaded, and the skill-tier ordering rules. +* [Hooks](./hooks.md): hooks defined by a plugin fire alongside SDK-registered hooks. +* [MCP Servers](./mcp.md): plugin-provided MCP servers integrate the same way as inline registrations. +* [Fleet Mode](./fleet-mode.md): plugin-provided agents are dispatchable as sub-agents. diff --git a/docs/index.md b/docs/index.md index 276d8d1e1..9abc943f5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -45,6 +45,7 @@ Guides for building with the SDK's capabilities. * [Custom Agents](./features/custom-agents.md): define specialized sub-agents * [MCP Servers](./features/mcp.md): integrate Model Context Protocol servers * [Skills](./features/skills.md): load reusable prompt modules +* [Plugin Directories](./features/plugin-directories.md): bundle skills, hooks, MCP servers, and agents as a single loadable plugin * [Image Input](./features/image-input.md): send images as attachments * [Streaming Events](./features/streaming-events.md): real-time event reference * [Steering & Queueing](./features/steering-and-queueing.md): message delivery modes From 3b464ac02755fd8a464f8971f424533124141289 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 May 2026 09:32:54 -0700 Subject: [PATCH 5/8] docs: address PR review feedback for accuracy Verified all 10 reviewer findings against source; all accurate. Applied fixes: - backend-services.md: Replace 5 instances of the non-existent TS top-level 'cliUrl' option with 'connection: RuntimeConnection.forUri(...)' (matches CopilotClientOptions in nodejs/src/types.ts). - multi-tenancy.md: Drop baseDirectory/sessionIdleTimeoutSeconds from the TS forUri sample and setCopilotHome/setSessionIdleTimeoutSeconds from the Java setCliUrl sample (both ignored for URI connections; documented inline). Fix SessionFsConfig field 'initialWorkingDirectory' -> 'initialCwd' (matches nodejs/src/types.ts:2068). Remove 'or cliUrl' from the TS row of the external-runtime table. - streaming-events.md: Add 'ws:/responses' to assistant.usage.apiEndpoint union (matches nodejs/src/generated/session-events.ts:231). - fleet-mode.md: Fix Rust crate path 'github_copilot' -> 'github_copilot_sdk' (rust/Cargo.toml lib name). - cloud-sessions.md: Replace invalid Rust async closure permission handler with Arc via ApproveAllHandler from handler module (matches rust/src/types.rs:1529 signature). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/cloud-sessions.md | 7 +++---- docs/features/fleet-mode.md | 2 +- docs/features/streaming-events.md | 2 +- docs/setup/backend-services.md | 16 ++++++++-------- docs/setup/multi-tenancy.md | 13 +++++++------ 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md index 684486c06..06dec42f5 100644 --- a/docs/features/cloud-sessions.md +++ b/docs/features/cloud-sessions.md @@ -169,17 +169,16 @@ try (var client = new CopilotClient()) { ### Rust ```rust +use std::sync::Arc; use github_copilot_sdk::{CloudSessionOptions, CloudSessionRepository, SessionConfig}; -use github_copilot_sdk::handler::PermissionResult; +use github_copilot_sdk::handler::ApproveAllHandler; let session = client.create_session( SessionConfig::default() .with_cloud(CloudSessionOptions::with_repository( CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), )) - .with_permission_handler(|_req, _inv| async { - Ok(PermissionResult::approve_once()) - }), + .with_permission_handler(Arc::new(ApproveAllHandler)), ).await?; ``` diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index 2de9e8806..26bad2ac5 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -152,7 +152,7 @@ if (result.Started) Rust ```rust -use github_copilot::generated::api_types::FleetStartRequest; +use github_copilot_sdk::generated::api_types::FleetStartRequest; let result = session .rpc() diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 460b4918e..f1b900118 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -315,7 +315,7 @@ Ephemeral. Token usage and cost information for an individual API call. | `duration` | `number` | | API call duration in milliseconds | | `initiator` | `string` | | What triggered this call (e.g., `"sub-agent"`); absent for user-initiated | | `apiCallId` | `string` | | Completion ID from the provider (e.g., `chatcmpl-abc123`) | -| `apiEndpoint` | `"/chat/completions" \| "/v1/messages" \| "/responses"` | | API endpoint used for the model call; useful for observability and cost attribution | +| `apiEndpoint` | `"/chat/completions" \| "/v1/messages" \| "/responses" \| "ws:/responses"` | | API endpoint used for the model call; useful for observability and cost attribution. `ws:/responses` is the websocket variant of the responses API | | `providerCallId` | `string` | | GitHub request tracing ID (`x-github-request-id`) | | `parentToolCallId` | `string` | | Set when usage originates from a sub-agent | | `quotaSnapshots` | `Record` | | Per-quota resource usage, keyed by quota identifier | diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index 8cbc8d27a..a50a64cef 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -125,10 +125,10 @@ Restart=always Node.js / TypeScript ```typescript -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; const client = new CopilotClient({ - cliUrl: "localhost:4321", + connection: RuntimeConnection.forUri("localhost:4321"), mode: "empty", }); @@ -324,7 +324,7 @@ Pass individual user tokens when creating sessions. See [GitHub OAuth](./github- ```typescript const client = new CopilotClient({ - cliUrl: "localhost:4321", + connection: RuntimeConnection.forUri("localhost:4321"), mode: "empty", }); @@ -351,7 +351,7 @@ Use your own API keys for the model provider. See [BYOK](../auth/byok.md) for de ```typescript const client = new CopilotClient({ - cliUrl: "localhost:4321", + connection: RuntimeConnection.forUri("localhost:4321"), }); const session = await client.createSession({ @@ -386,14 +386,14 @@ flowchart TB ```typescript import express from "express"; -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; const app = express(); app.use(express.json()); // Single shared CLI connection for multi-user server mode const client = new CopilotClient({ - cliUrl: process.env.CLI_URL || "localhost:4321", + connection: RuntimeConnection.forUri(process.env.CLI_URL || "localhost:4321"), mode: "empty", }); @@ -426,10 +426,10 @@ app.listen(3000); ### Background worker ```typescript -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; const client = new CopilotClient({ - cliUrl: process.env.CLI_URL || "localhost:4321", + connection: RuntimeConnection.forUri(process.env.CLI_URL || "localhost:4321"), }); // Process jobs from a queue diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md index 3c84110ce..ccc3432a4 100644 --- a/docs/setup/multi-tenancy.md +++ b/docs/setup/multi-tenancy.md @@ -38,10 +38,11 @@ Do not use the default `mode: "copilot-cli"` for shared servers. That mode is in ```typescript import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; +// baseDirectory and sessionIdleTimeoutSeconds apply when the SDK spawns the +// runtime. With RuntimeConnection.forUri(...) configure COPILOT_HOME and the +// idle timeout on the runtime process itself. const client = new CopilotClient({ mode: "empty", - baseDirectory: `/var/lib/my-app/copilot/${runtimeInstanceId}`, - sessionIdleTimeoutSeconds: 900, connection: RuntimeConnection.forUri(process.env.COPILOT_RUNTIME_URL!), }); @@ -202,10 +203,10 @@ import java.util.List; import com.github.copilot.CopilotClient; import com.github.copilot.rpc.*; +// setCopilotHome and setSessionIdleTimeoutSeconds are ignored when +// setCliUrl is used; configure those on the runtime process instead. var client = new CopilotClient(new CopilotClientOptions() .setMode(CopilotClientMode.EMPTY) - .setCopilotHome("/var/lib/my-app/copilot/" + runtimeInstanceId) - .setSessionIdleTimeoutSeconds(900) .setCliUrl(runtimeUrl) ); @@ -292,7 +293,7 @@ When the SDK connects to an already-running runtime with `RuntimeConnection.forU const client = new CopilotClient({ mode: "empty", sessionFs: { - initialWorkingDirectory: "/workspace", + initialCwd: "/workspace", sessionStatePath: "/session-state", conventions: "posix", }, @@ -319,7 +320,7 @@ Use an external runtime connection when multiple SDK clients should share one al | Language | External runtime connection | |----------|-----------------------------| -| TypeScript | `RuntimeConnection.forUri(url)` or `cliUrl` | +| TypeScript | `RuntimeConnection.forUri(url)` | | Python | `RuntimeConnection.for_uri(url)` | | Go | `copilot.UriConnection{URL: url}` | | .NET | `RuntimeConnection.ForUri(url)` | From 332df7df5c13b9378f4e0db6a0d37ed0a2dbb9fb Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 May 2026 10:17:57 -0700 Subject: [PATCH 6/8] docs: fix docs-validate CI failures (Java + Python wrappers, remove last skip) CI Validate-* jobs run with mvn installed (so real Java errors surfaced) and mypy strict (so undefined module-level names surfaced): - features/fleet-mode.md Python sub-agent events: wrap visible snippet with hidden async main() that constructs session via CopilotClient. - setup/multi-tenancy.md Java: add hidden compilable wrapper that defines runtimeUrl/user/requestId stubs and switches wildcard import to fully-qualified com.github.copilot.rpc imports. - features/plugin-directories.md Java: fix wrong import (com.github.copilot.CopilotClientOptions -> com.github.copilot.rpc.CopilotClientOptions) and wrap in hidden compilable class. - features/plugin-directories.md Rust: remove last docs-validate: skip; wrap with hidden tokio::main and keep visible excerpt. All non-Java validators now clean locally; only env-only mvn errors remain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/fleet-mode.md | 25 +++++++++++++++++++ docs/features/plugin-directories.md | 38 +++++++++++++++++++++++++++-- docs/setup/multi-tenancy.md | 32 +++++++++++++++++++++++- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index 26bad2ac5..083e35c41 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -265,6 +265,31 @@ session.on((event) => {
Python + +```python +import asyncio +from copilot import CopilotClient +from copilot.session import PermissionHandler + +async def main(): + client = CopilotClient() + await client.start() + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + + def handle_event(event): + if event.type == "subagent.started": + print(f"Started {event.data.agent_display_name}") + elif event.type == "subagent.completed": + print(f"Completed {event.data.agent_display_name}") + + unsubscribe = session.on(handle_event) + +asyncio.run(main()) +``` + + ```python def handle_event(event): if event.type == "subagent.started": diff --git a/docs/features/plugin-directories.md b/docs/features/plugin-directories.md index 66dc4b8f5..ccd95df95 100644 --- a/docs/features/plugin-directories.md +++ b/docs/features/plugin-directories.md @@ -170,10 +170,27 @@ await client.StartAsync();
Java + ```java import com.github.copilot.CopilotClient; -import com.github.copilot.CopilotClientOptions; +import com.github.copilot.rpc.CopilotClientOptions; + +public class PluginDirectoriesExample { + public static void main(String[] args) throws Exception { + var options = new CopilotClientOptions() + .setCliArgs(new String[] { + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + }); + + var client = new CopilotClient(options); + client.start().get(); + } +} +``` + +```java var options = new CopilotClientOptions() .setCliArgs(new String[] { "--plugin-dir", "./plugins/code-reviewer", @@ -189,7 +206,24 @@ client.start().get();
Rust - + +```rust +use github_copilot_sdk::{Client, ClientOptions}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _client = Client::start( + ClientOptions::new().with_extra_args([ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + ]), + ) + .await?; + Ok(()) +} +``` + + ```rust use github_copilot_sdk::{Client, ClientOptions}; diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md index ccc3432a4..6f08ecddf 100644 --- a/docs/setup/multi-tenancy.md +++ b/docs/setup/multi-tenancy.md @@ -198,11 +198,41 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
Java + ```java import java.util.List; import com.github.copilot.CopilotClient; -import com.github.copilot.rpc.*; +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.CopilotClientMode; +import com.github.copilot.rpc.SessionConfig; + +public class MultiTenancyExample { + record User(String id, String gitHubToken) {} + + public static void main(String[] args) throws Exception { + String runtimeUrl = "http://localhost:4321"; + String requestId = "req-1"; + User user = new User("u1", "ghu_token"); + + // setCopilotHome and setSessionIdleTimeoutSeconds are ignored when + // setCliUrl is used; configure those on the runtime process instead. + var client = new CopilotClient(new CopilotClientOptions() + .setMode(CopilotClientMode.EMPTY) + .setCliUrl(runtimeUrl) + ); + + var session = client.createSession(new SessionConfig() + .setSessionId("user-" + user.id() + "-" + requestId) + .setModel("gpt-4.1") + .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket")) + .setGitHubToken(user.gitHubToken()) + ).get(); + } +} +``` + +```java // setCopilotHome and setSessionIdleTimeoutSeconds are ignored when // setCliUrl is used; configure those on the runtime process instead. var client = new CopilotClient(new CopilotClientOptions() From 96bfbe611f34f5e75355e1cdf2e3b9aabc5c26a0 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Fri, 29 May 2026 11:32:57 -0700 Subject: [PATCH 7/8] docs(cloud-sessions): add send-after-start, URL surfacing, and troubleshooting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover two real-world gotchas that the existing guide does not mention: 1. "Sending the first prompt" — explains that createSession returns before the remote worker connects, so the first session.send must await session.start{producer:'copilot-agent'}. Without it the runtime silently swallows the prompt (RemoteSession.sendForSchema is fire-and-forget) and the developer sees a resolved messageId but no assistant.* events and no prompt in Mission Control. Also notes that streaming:true is needed for assistant.message_delta. 2. "Accessing the Mission Control URL" — cloud sessions auto-publish a shareable URL via session.info{infoType:'remote', url}. Apps should subscribe to that event rather than calling remote.enable(), which is only for promoting local sessions. Add three matching rows to the Troubleshooting table and a Streaming Events link in See Also. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/cloud-sessions.md | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md index 06dec42f5..0bfd05f28 100644 --- a/docs/features/cloud-sessions.md +++ b/docs/features/cloud-sessions.md @@ -184,6 +184,66 @@ let session = client.create_session( +## Sending the first prompt + +Cloud sessions initialize in two phases: `createSession` resolves as soon as Mission Control has reserved a task, but the remote `copilot-agent` worker takes another second or two to connect and emit `session.start`. If you call `session.send` before that, the runtime's `RemoteSession.send` throws `"Remote session is still starting"` — but the schema wrapper is fire-and-forget and **silently swallows the error** while still returning a fresh `messageId` to your code. The prompt is dropped on the server and never reaches the worker. + +To send reliably, subscribe to events **before** sending and await the first `session.start` event whose `producer` is `"copilot-agent"`: + + +```typescript +import { CopilotClient, type CopilotSession } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session: CopilotSession = await client.createSession({ + streaming: true, // required for assistant.message_delta to fire + cloud: { repository: { owner: "github", name: "copilot-sdk" } }, + onPermissionRequest: async () => ({ kind: "approve-once" }), +}); + +// Subscribe BEFORE sending so you don't miss the start event. +const ready = new Promise((resolve) => { + const off = session.on("session.start", (event) => { + if (event.data?.producer === "copilot-agent") { + off(); + resolve(); + } + }); +}); + +await ready; +await session.send({ prompt: "Summarize the README" }); +``` + +A few notes: + +* Set `streaming: true` on `createSession` so the runtime emits `assistant.message_delta` events. Without it, the only assistant signal you get is the final `assistant.message` — fine for batch use, but the chat will look frozen if you're rendering a live UI. See [Streaming Events](./streaming-events.md). +* Only the **first** `session.send` is sensitive to this race. Subsequent sends on the same session work normally because the runtime keeps `hasSessionStarted` set for the life of the session. +* Apply a timeout (e.g. 60 s) around the `ready` promise so a stuck Mission Control provisioning doesn't hang your app forever. +* The same pattern works in every SDK language — subscribe to `session.start`, check `producer === "copilot-agent"`, then call `send`. + +## Accessing the Mission Control URL + +Cloud sessions are inherently remote: once the worker connects, Mission Control publishes the session at `https://github.com/copilot/tasks/{sessionId}` and the runtime emits a `session.info` event with the URL. You do **not** need to call `remote.enable()` — that API is only for promoting a local session to Mission Control. + +Capture the URL by subscribing to `session.info` and filtering by `infoType: "remote"`: + + +```typescript +session.on("session.info", (event) => { + if (event.data?.infoType === "remote" && event.data.url) { + console.log("Open from web or mobile:", event.data.url); + // e.g. surface in your UI as a shareable link or QR code. + } +}); +``` + +The event fires shortly after `session.start`. If your renderer mounts after the event has already fired, persist the URL alongside the session record in your app's state and rehydrate on remount — the runtime does not re-emit `session.info` on its own. + +For the same wiring on local sessions promoted via `remote: true`, see [Remote Sessions](./remote-sessions.md). + ## Repository association The `cloud.repository` object associates the cloud session with a GitHub repository: @@ -312,9 +372,13 @@ Use remote sessions when the session should execute where the SDK runtime is alr | Session creates without repository context | `cloud.repository` was omitted | Pass `owner`, `name`, and optionally `branch` | | Resume ignores a new `cloud` option | `cloud` only applies to new sessions | Resume the existing session normally | | Confusion with sandbox settings | Windows sandbox and cloud sessions are separate | Do not use `SANDBOX=true` for cloud execution | +| `session.send` resolves with a `messageId` but no `assistant.*` events fire and Mission Control shows no prompt | The session.send raced ahead of `session.start` from the remote worker; the runtime swallowed the prompt | Await the first `session.start` event with `producer === "copilot-agent"` before sending. See [Sending the first prompt](#sending-the-first-prompt) | +| Live UI never updates even though the cloud worker is processing | `streaming` was not set on `createSession`, so only the final `assistant.message` is emitted | Set `streaming: true` on `createSession` and re-launch | +| Cloud session works but no shareable URL appears in your UI | App never subscribed to `session.info` for the URL | Subscribe to `session.info` and filter `infoType === "remote"`. See [Accessing the Mission Control URL](#accessing-the-mission-control-url) | ## See also * [Remote Sessions](./remote-sessions.md): share locally hosted sessions through Mission Control +* [Streaming Events](./streaming-events.md): subscribe to `assistant.*` deltas for live UI rendering * [Multi-tenancy](../setup/multi-tenancy.md): integration IDs and server deployment patterns * [Authentication](../auth/index.md): configure GitHub authentication for SDK sessions From 2bb6f178f77acf3551391bc21b6b044d7107a0fc Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Fri, 29 May 2026 14:31:48 -0700 Subject: [PATCH 8/8] Update cloud-sessions.md --- docs/features/cloud-sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md index 0bfd05f28..6f9564911 100644 --- a/docs/features/cloud-sessions.md +++ b/docs/features/cloud-sessions.md @@ -286,7 +286,7 @@ Do not pass `cloud` again on resume. The saved session metadata determines that ## Org policies and entitlements -Cloud session creation can fail when the user or organization is not entitled to cloud-agent execution or when organization-level policies block the flow. In particular, policies for remote control or viewing sessions from cloud surfaces can prevent Mission Control from creating the cloud task. +Cloud session creation can fail when the user or organization is not entitled to cloud-agent execution or when organization-level policies block the flow. In particular, policies for cloud sandbox can prevent clients from creating the cloud task. When this happens, the runtime reports a `"policy_blocked"` failure reason for cloud task creation. Treat this as an authorization or policy outcome, not as a transient infrastructure failure.