diff --git a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png index 4ce852f914fa..9263455a5ac2 100644 Binary files a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png and b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png differ diff --git a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png index 5161be682a61..6fe2706dff63 100644 Binary files a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png and b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png differ diff --git a/assets/images/help/copilot/copilot-sdk/setup-multi-tenancy-diagram-0.png b/assets/images/help/copilot/copilot-sdk/setup-multi-tenancy-diagram-0.png new file mode 100644 index 000000000000..57d30960d07d Binary files /dev/null and b/assets/images/help/copilot/copilot-sdk/setup-multi-tenancy-diagram-0.png differ diff --git a/content/copilot/how-tos/copilot-sdk/auth/authenticate.md b/content/copilot/how-tos/copilot-sdk/auth/authenticate.md index b6ab9d24add8..4175e5067814 100644 --- a/content/copilot/how-tos/copilot-sdk/auth/authenticate.md +++ b/content/copilot/how-tos/copilot-sdk/auth/authenticate.md @@ -93,7 +93,7 @@ await using var client = new CopilotClient(); {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.CopilotClient; // Default: uses logged-in user credentials var client = new CopilotClient(); @@ -166,7 +166,7 @@ func main() { import copilot "github.com/github/copilot-sdk/go" client := copilot.NewClient(&copilot.ClientOptions{ - GithubToken: userAccessToken, // Token from OAuth flow + GitHubToken: userAccessToken, // Token from OAuth flow UseLoggedInUser: copilot.Bool(false), // Don't use stored CLI credentials }) ``` @@ -198,9 +198,11 @@ await using var client = new CopilotClient(new CopilotClientOptions {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(new CopilotClientOptions() .setGitHubToken(userAccessToken) // Token from OAuth flow @@ -292,13 +294,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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy). + ## Disabling auto-login To prevent the SDK from automatically using stored credentials or `gh` CLI auth, use the `useLoggedInUser: false` option: @@ -365,8 +369,8 @@ await using var client = new CopilotClient(new CopilotClientOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(new CopilotClientOptions() .setUseLoggedInUser(false) // Only use explicit tokens diff --git a/content/copilot/how-tos/copilot-sdk/auth/byok.md b/content/copilot/how-tos/copilot-sdk/auth/byok.md index db14d167386c..9a37fb8202ab 100644 --- a/content/copilot/how-tos/copilot-sdk/auth/byok.md +++ b/content/copilot/how-tos/copilot-sdk/auth/byok.md @@ -126,7 +126,7 @@ func main() { Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: "https://your-resource.openai.azure.com/openai/v1/", - WireApi: "responses", // Use "completions" for older models + WireAPI: "responses", // Use "completions" for older models APIKey: os.Getenv("FOUNDRY_API_KEY"), }, }) @@ -177,9 +177,8 @@ Console.WriteLine(response?.Data.Content); {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(); client.start().get(); @@ -214,15 +213,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 @@ -450,8 +451,8 @@ var client = new CopilotClient(new CopilotClientOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -489,6 +490,7 @@ Some Copilot features may behave differently with BYOK: * **Model availability** - Only models supported by your provider are available * **Rate limiting** - Subject to your provider's rate limits, not Copilot's * **Usage tracking** - Usage is tracked by your provider, not GitHub Copilot +* **Premium requests** - Do not count against Copilot premium request quotas ### Provider-specific limitations diff --git a/content/copilot/how-tos/copilot-sdk/features/cloud-sessions.md b/content/copilot/how-tos/copilot-sdk/features/cloud-sessions.md new file mode 100644 index 000000000000..fe36d3c6f2d0 --- /dev/null +++ b/content/copilot/how-tos/copilot-sdk/features/cloud-sessions.md @@ -0,0 +1,403 @@ +--- +title: Cloud sessions +shortTitle: Cloud Sessions +intro: >- + Run Copilot sessions on GitHub-hosted compute through Mission Control instead + of local CLI sessions. +versions: + fpt: '*' + ghec: '*' +contentType: how-tos +--- + + + + +## 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 ( + CloudSessionOptions, + CloudSessionRepository, + CopilotClient, + 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 + + + +```golang +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 +} +``` + + + +```golang +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 std::sync::Arc; +use github_copilot_sdk::{CloudSessionOptions, CloudSessionRepository, SessionConfig}; +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(Arc::new(ApproveAllHandler)), +).await?; +``` + + + +## 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/streaming-events). +* 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/remote-sessions). + +## 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 +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. + +## 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 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. + +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 } }); +} 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy). + +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 | +| `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 + +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/remote-sessions): share locally hosted sessions through Mission Control +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/streaming-events): subscribe to `assistant.*` deltas for live UI rendering +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy): integration IDs and server deployment patterns +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/auth): configure GitHub authentication for SDK sessions diff --git a/content/copilot/how-tos/copilot-sdk/features/custom-agents.md b/content/copilot/how-tos/copilot-sdk/features/custom-agents.md index 882b08f8915d..d93076614715 100644 --- a/content/copilot/how-tos/copilot-sdk/features/custom-agents.md +++ b/content/copilot/how-tos/copilot-sdk/features/custom-agents.md @@ -3,7 +3,7 @@ title: Custom agents and sub-agent orchestration shortTitle: Custom Agents intro: >- Define specialized agents with scoped tools and prompts, then let Copilot - orchestrate them as sub-agents within a single session. + orchestrate them as sub-agents within a single session. versions: fpt: '*' ghec: '*' @@ -205,9 +205,8 @@ await using var session = await client.CreateSessionAsync(new SessionConfig {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; try (var client = new CopilotClient()) { @@ -380,7 +379,7 @@ var session = await client.CreateSessionAsync(new SessionConfig ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; import java.util.List; var session = client.createSession( @@ -638,6 +637,8 @@ await session.SendAndWaitAsync(new MessageOptions {% endcodetab %} {% codetab java %} + + ```java session.on(event -> { if (event instanceof SubagentStartedEvent e) { diff --git a/content/copilot/how-tos/copilot-sdk/features/fleet-mode.md b/content/copilot/how-tos/copilot-sdk/features/fleet-mode.md new file mode 100644 index 000000000000..817959982f40 --- /dev/null +++ b/content/copilot/how-tos/copilot-sdk/features/fleet-mode.md @@ -0,0 +1,346 @@ +--- +title: Fleet mode +shortTitle: Fleet Mode +intro: >- + Use fleet mode to split work across multiple sub-agents and combine their + results in one parent session. +versions: + fpt: '*' + ghec: '*' +contentType: how-tos +--- + + + + +## 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. + +{% codetabs %} +{% codetab 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"); +} +``` + +{% endcodetab %} +{% codetab 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") +``` + +{% endcodetab %} +{% codetab go %} + +```golang +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") + } +} +``` + +```golang +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") +} +``` + +{% endcodetab %} +{% codetab dotnet %} + +```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."); + +if (result.Started) +{ + Console.WriteLine("Fleet mode started"); +} +``` + +{% endcodetab %} +{% codetab rust %} + +```rust +use github_copilot_sdk::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"); +} +``` + +{% endcodetab %} +{% endcodetabs %} + +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 +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. + +## 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`. + +{% codetabs %} +{% codetab 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}`); + } +}); +``` + +{% endcodetab %} +{% codetab 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": + 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) +``` + +{% endcodetab %} +{% endcodetabs %} + +For hook configuration that is already exposed at the SDK layer, see [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/hooks). For sub-agent event payloads, see [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/custom-agents). + +## 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 + +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/custom-agents) +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/hooks) diff --git a/content/copilot/how-tos/copilot-sdk/features/hooks.md b/content/copilot/how-tos/copilot-sdk/features/hooks.md index e124ea5c8459..1e15f414f7cc 100644 --- a/content/copilot/how-tos/copilot-sdk/features/hooks.md +++ b/content/copilot/how-tos/copilot-sdk/features/hooks.md @@ -203,9 +203,8 @@ var session = await client.CreateSessionAsync(new SessionConfig {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.concurrent.CompletableFuture; try (var client = new CopilotClient()) { @@ -408,14 +407,16 @@ var session = await client.CreateSessionAsync(new SessionConfig {% endcodetab %} {% codetab java %} + + ```java import java.util.Set; import java.util.concurrent.CompletableFuture; -import com.github.copilot.sdk.PermissionHandler; -import com.github.copilot.sdk.SessionConfig; -import com.github.copilot.sdk.SessionHooks; -import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.PreToolUseHookOutput; var readOnlyTools = Set.of("read_file", "glob", "grep", "view"); var hooks = new SessionHooks() diff --git a/content/copilot/how-tos/copilot-sdk/features/image-input.md b/content/copilot/how-tos/copilot-sdk/features/image-input.md index 7ca964146ccb..5f95fc5db566 100644 --- a/content/copilot/how-tos/copilot-sdk/features/image-input.md +++ b/content/copilot/how-tos/copilot-sdk/features/image-input.md @@ -112,7 +112,7 @@ func main() { session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ - &copilot.UserMessageAttachmentFile{ + &copilot.AttachmentFile{ DisplayName: "screenshot.png", Path: path, }, @@ -137,7 +137,7 @@ path := "/absolute/path/to/screenshot.png" session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ - &copilot.UserMessageAttachmentFile{ + &copilot.AttachmentFile{ DisplayName: "screenshot.png", Path: path, }, @@ -167,9 +167,9 @@ public static class ImageInputExample await session.SendAsync(new MessageOptions { Prompt = "Describe what you see in this image", - Attachments = new List + Attachments = new List { - new UserMessageAttachmentFile + new AttachmentFile { Path = "/absolute/path/to/screenshot.png", DisplayName = "screenshot.png", @@ -195,9 +195,9 @@ await using var session = await client.CreateSessionAsync(new SessionConfig await session.SendAsync(new MessageOptions { Prompt = "Describe what you see in this image", - Attachments = new List + Attachments = new List { - new UserMessageAttachmentFile + new AttachmentFile { Path = "/absolute/path/to/screenshot.png", DisplayName = "screenshot.png", @@ -210,9 +210,8 @@ await session.SendAsync(new MessageOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; try (var client = new CopilotClient()) { @@ -326,7 +325,7 @@ func main() { session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ - &copilot.UserMessageAttachmentBlob{ + &copilot.AttachmentBlob{ Data: base64ImageData, MIMEType: mimeType, DisplayName: &displayName, @@ -342,7 +341,7 @@ displayName := "screenshot.png" session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ - &copilot.UserMessageAttachmentBlob{ + &copilot.AttachmentBlob{ Data: base64ImageData, // base64-encoded string MIMEType: mimeType, DisplayName: &displayName, @@ -374,9 +373,9 @@ public static class BlobAttachmentExample await session.SendAsync(new MessageOptions { Prompt = "Describe what you see in this image", - Attachments = new List + Attachments = new List { - new UserMessageAttachmentBlob + new AttachmentBlob { Data = base64ImageData, MimeType = "image/png", @@ -392,9 +391,9 @@ public static class BlobAttachmentExample await session.SendAsync(new MessageOptions { Prompt = "Describe what you see in this image", - Attachments = new List + Attachments = new List { - new UserMessageAttachmentBlob + new AttachmentBlob { Data = base64ImageData, MimeType = "image/png", @@ -408,9 +407,8 @@ await session.SendAsync(new MessageOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; try (var client = new CopilotClient()) { diff --git a/content/copilot/how-tos/copilot-sdk/features/index.md b/content/copilot/how-tos/copilot-sdk/features/index.md index 859367e45e68..4e5d83814a03 100644 --- a/content/copilot/how-tos/copilot-sdk/features/index.md +++ b/content/copilot/how-tos/copilot-sdk/features/index.md @@ -12,10 +12,13 @@ redirect_from: contentType: how-tos children: - /agent-loop + - /cloud-sessions - /custom-agents + - /fleet-mode - /hooks - /image-input - /mcp + - /plugin-directories - /remote-sessions - /session-persistence - /skills diff --git a/content/copilot/how-tos/copilot-sdk/features/plugin-directories.md b/content/copilot/how-tos/copilot-sdk/features/plugin-directories.md new file mode 100644 index 000000000000..f66586d55a10 --- /dev/null +++ b/content/copilot/how-tos/copilot-sdk/features/plugin-directories.md @@ -0,0 +1,357 @@ +--- +title: Plugin directories +shortTitle: Plugin Directories +intro: >- + Use plugin directories to load skills, hooks, MCP servers, custom agents, and + LSP settings from a single manifest. +versions: + fpt: '*' + ghec: '*' +contentType: how-tos +--- + + + + +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: + +```text +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. + +{% codetabs %} +{% codetab 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(); +``` + +{% endcodetab %} +{% codetab 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() +``` + +{% endcodetab %} +{% codetab go %} + +```golang +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 + } +} +``` + +```golang +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 +} +``` + +{% endcodetab %} +{% codetab dotnet %} + +```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(); +``` + +{% endcodetab %} +{% codetab java %} + +```java +import com.github.copilot.CopilotClient; +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", + "--plugin-dir", "./plugins/lint-fix", + }); + +var client = new CopilotClient(options); +client.start().get(); +``` + +{% endcodetab %} +{% codetab 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}; + +let client = Client::start( + ClientOptions::new().with_extra_args([ + "--plugin-dir", "./plugins/code-reviewer", + "--plugin-dir", "./plugins/lint-fix", + ]), +) +.await?; +``` + +{% endcodetab %} +{% endcodetabs %} + +> 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/fleet-mode): 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 + +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/custom-agents): write agents that ship inside a plugin's `agents/` folder. +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/skills): how `SKILL.md` files are loaded, and the skill-tier ordering rules. +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/hooks): hooks defined by a plugin fire alongside SDK-registered hooks. +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/mcp): plugin-provided MCP servers integrate the same way as inline registrations. +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/fleet-mode): plugin-provided agents are dispatchable as sub-agents. diff --git a/content/copilot/how-tos/copilot-sdk/features/remote-sessions.md b/content/copilot/how-tos/copilot-sdk/features/remote-sessions.md index 1a3e6a6f4b42..d2b32e692c9c 100644 --- a/content/copilot/how-tos/copilot-sdk/features/remote-sessions.md +++ b/content/copilot/how-tos/copilot-sdk/features/remote-sessions.md @@ -17,6 +17,8 @@ contentType: how-tos +For running sessions on GitHub-hosted compute, see [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/cloud-sessions). + ## Prerequisites * The user must be authenticated (GitHub token or logged-in user) @@ -140,97 +142,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 - - - -```golang -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. @@ -316,6 +227,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/content/copilot/how-tos/copilot-sdk/features/skills.md b/content/copilot/how-tos/copilot-sdk/features/skills.md index ac9c6ed77f10..3e2fd99814d4 100644 --- a/content/copilot/how-tos/copilot-sdk/features/skills.md +++ b/content/copilot/how-tos/copilot-sdk/features/skills.md @@ -151,9 +151,8 @@ await session.SendAndWaitAsync(new MessageOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; try (var client = new CopilotClient()) { @@ -275,8 +274,10 @@ var session = await client.CreateSessionAsync(new SessionConfig {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; import java.util.List; var session = client.createSession( diff --git a/content/copilot/how-tos/copilot-sdk/features/steering-and-queueing.md b/content/copilot/how-tos/copilot-sdk/features/steering-and-queueing.md index 428c139d7682..85ef4660148e 100644 --- a/content/copilot/how-tos/copilot-sdk/features/steering-and-queueing.md +++ b/content/copilot/how-tos/copilot-sdk/features/steering-and-queueing.md @@ -169,9 +169,8 @@ await session.SendAsync(new MessageOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; try (var client = new CopilotClient()) { client.start().get(); @@ -402,9 +401,8 @@ await session.SendAsync(new MessageOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; try (var client = new CopilotClient()) { client.start().get(); diff --git a/content/copilot/how-tos/copilot-sdk/features/streaming-events.md b/content/copilot/how-tos/copilot-sdk/features/streaming-events.md index 021533bd18ac..202b768e05a6 100644 --- a/content/copilot/how-tos/copilot-sdk/features/streaming-events.md +++ b/content/copilot/how-tos/copilot-sdk/features/streaming-events.md @@ -164,6 +164,8 @@ session.On(evt => {% endcodetab %} {% codetab java %} + + ```java // All events session.on(event -> System.out.println(event.getType())); @@ -284,6 +286,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" \| "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 | @@ -728,7 +731,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/content/copilot/how-tos/copilot-sdk/getting-started.md b/content/copilot/how-tos/copilot-sdk/getting-started.md index 917db8fa4aa6..f226f43ae757 100644 --- a/content/copilot/how-tos/copilot-sdk/getting-started.md +++ b/content/copilot/how-tos/copilot-sdk/getting-started.md @@ -32,7 +32,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](/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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/bundled-cli). 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+ @@ -263,7 +263,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 @@ -320,10 +320,11 @@ dotnet run Create `HelloCopilot.java`: + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; public class HelloCopilot { public static void main(String[] args) throws Exception { @@ -504,7 +505,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 @@ -576,10 +577,11 @@ await session.SendAndWaitAsync(new MessageOptions { Prompt = "Tell me a short jo Update `HelloCopilot.java`: + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; public class HelloCopilot { public static void main(String[] args) throws Exception { @@ -827,6 +829,8 @@ unsubscribe.Dispose(); {% endcodetab %} {% codetab java %} + + ```java // Subscribe to all events var unsubscribe = session.on(event -> { @@ -1051,7 +1055,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; @@ -1063,27 +1067,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 { @@ -1176,10 +1181,11 @@ await session.SendAndWaitAsync(new MessageOptions Update `HelloCopilot.java`: + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; import java.util.Map; @@ -1502,7 +1508,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; @@ -1523,27 +1529,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 { @@ -1672,10 +1679,11 @@ dotnet run Create `WeatherAssistant.java`: + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; import java.util.Map; @@ -1946,9 +1954,9 @@ import ( func main() { ctx := context.Background() - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.UriConnection{URL: "localhost:4321"}, - }) + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.URIConnection{URL: "localhost:4321"}, + }) if err := client.Start(ctx); err != nil { log.Fatal(err) @@ -1966,7 +1974,7 @@ func main() { import copilot "github.com/github/copilot-sdk/go" client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.UriConnection{URL: "localhost:4321"}, + Connection: copilot.URIConnection{URL: "localhost:4321"}, }) if err := client.Start(ctx); err != nil { @@ -1999,7 +2007,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?; // ... ``` @@ -2027,8 +2035,8 @@ await using var session = await client.CreateSessionAsync(new() {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient( new CopilotClientOptions().setCliUrl("localhost:4321") @@ -2045,7 +2053,7 @@ var session = client.createSession( {% endcodetab %} {% endcodetabs %} -**Note:** When `cli_url` / `cliUrl` / Go's `UriConnection` is provided, or Rust uses `Transport::External`, the SDK will not spawn or manage a CLI process - it will only connect to the existing server at the specified URL. +**Note:** When `cli_url` / `cliUrl` / Go's `URIConnection` is provided, or Rust uses `Transport::External`, the SDK will not spawn or manage a CLI process - it will only connect to the existing server at the specified URL. ## Telemetry and observability @@ -2146,8 +2154,8 @@ No extra dependencies—uses built-in `System.Diagnostics.Activity`. ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(new CopilotClientOptions() .setTelemetry(new TelemetryConfig() diff --git a/content/copilot/how-tos/copilot-sdk/hooks/error-handling.md b/content/copilot/how-tos/copilot-sdk/hooks/error-handling.md index 1f76b6ed69b1..d3b3d762752c 100644 --- a/content/copilot/how-tos/copilot-sdk/hooks/error-handling.md +++ b/content/copilot/how-tos/copilot-sdk/hooks/error-handling.md @@ -103,6 +103,8 @@ public delegate Task ErrorOccurredHandler( {% endcodetab %} {% codetab java %} + + ```java // Note: Java SDK does not have an onErrorOccurred hook. // Use EventErrorPolicy and EventErrorHandler instead: @@ -263,9 +265,11 @@ var session = await client.CreateSessionAsync(new SessionConfig {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.*; +import com.github.copilot.rpc.*; // Note: Java SDK does not have an onErrorOccurred hook. // Use EventErrorPolicy and EventErrorHandler instead: diff --git a/content/copilot/how-tos/copilot-sdk/hooks/hooks-overview.md b/content/copilot/how-tos/copilot-sdk/hooks/hooks-overview.md index a883c7dba021..ab46314b4ddc 100644 --- a/content/copilot/how-tos/copilot-sdk/hooks/hooks-overview.md +++ b/content/copilot/how-tos/copilot-sdk/hooks/hooks-overview.md @@ -166,8 +166,8 @@ var session = await client.CreateSessionAsync(new SessionConfig {% codetab java %} ```java -import com.github.copilot.sdk.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.*; +import com.github.copilot.rpc.*; import java.util.concurrent.CompletableFuture; try (var client = new CopilotClient()) { diff --git a/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md b/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md index 4afbcd9839b9..7fa71e2dcae3 100644 --- a/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md +++ b/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md @@ -115,9 +115,23 @@ public delegate Task PostToolUseHandler( {% codetab java %} ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; +import java.util.concurrent.CompletableFuture; -PostToolUseHandler postToolUseHandler; +public class PostToolUseSignature { + PostToolUseHandler handler = (PostToolUseHookInput input, HookInvocation invocation) -> + CompletableFuture.completedFuture(null); + public static void main(String[] args) {} +} +``` + +```java +@FunctionalInterface +public interface PostToolUseHandler { + CompletableFuture handle( + PostToolUseHookInput input, + HookInvocation invocation); +} ``` {% endcodetab %} @@ -269,9 +283,11 @@ var session = await client.CreateSessionAsync(new SessionConfig {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.*; +import com.github.copilot.rpc.*; import java.util.concurrent.CompletableFuture; var hooks = new SessionHooks() diff --git a/content/copilot/how-tos/copilot-sdk/hooks/pre-tool-use.md b/content/copilot/how-tos/copilot-sdk/hooks/pre-tool-use.md index 7adb0007292d..a334078c36a9 100644 --- a/content/copilot/how-tos/copilot-sdk/hooks/pre-tool-use.md +++ b/content/copilot/how-tos/copilot-sdk/hooks/pre-tool-use.md @@ -102,9 +102,23 @@ public delegate Task PreToolUseHandler( {% codetab java %} ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; +import java.util.concurrent.CompletableFuture; -PreToolUseHandler preToolUseHandler; +public class PreToolUseSignature { + PreToolUseHandler handler = (PreToolUseHookInput input, HookInvocation invocation) -> + CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + public static void main(String[] args) {} +} +``` + +```java +@FunctionalInterface +public interface PreToolUseHandler { + CompletableFuture handle( + PreToolUseHookInput input, + HookInvocation invocation); +} ``` {% endcodetab %} @@ -139,6 +153,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) @@ -265,9 +296,11 @@ var session = await client.CreateSessionAsync(new SessionConfig {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.*; +import com.github.copilot.rpc.*; import java.util.concurrent.CompletableFuture; var hooks = new SessionHooks() diff --git a/content/copilot/how-tos/copilot-sdk/hooks/session-lifecycle.md b/content/copilot/how-tos/copilot-sdk/hooks/session-lifecycle.md index 295395df6f89..0c9b2eaf8463 100644 --- a/content/copilot/how-tos/copilot-sdk/hooks/session-lifecycle.md +++ b/content/copilot/how-tos/copilot-sdk/hooks/session-lifecycle.md @@ -108,9 +108,23 @@ public delegate Task SessionStartHandler( {% codetab java %} ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; +import java.util.concurrent.CompletableFuture; + +public class SessionStartSignature { + SessionStartHandler handler = (SessionStartHookInput input, HookInvocation invocation) -> + CompletableFuture.completedFuture(null); + public static void main(String[] args) {} +} +``` -SessionStartHandler sessionStartHandler; +```java +@FunctionalInterface +public interface SessionStartHandler { + CompletableFuture handle( + SessionStartHookInput input, + HookInvocation invocation); +} ``` {% endcodetab %} @@ -308,9 +322,23 @@ public delegate Task SessionEndHandler( {% codetab java %} ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; +import java.util.concurrent.CompletableFuture; + +public class SessionEndSignature { + SessionEndHandler handler = (SessionEndHookInput input, HookInvocation invocation) -> + CompletableFuture.completedFuture(null); + public static void main(String[] args) {} +} +``` -SessionEndHandler sessionEndHandler; +```java +@FunctionalInterface +public interface SessionEndHandler { + CompletableFuture handle( + SessionEndHookInput input, + HookInvocation invocation); +} ``` {% endcodetab %} diff --git a/content/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted.md b/content/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted.md index 42246a31884c..9fa0979bf9c1 100644 --- a/content/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted.md +++ b/content/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted.md @@ -104,9 +104,23 @@ public delegate Task UserPromptSubmittedHandler( {% codetab java %} ```java -import com.github.copilot.sdk.json.*; +import com.github.copilot.rpc.*; +import java.util.concurrent.CompletableFuture; -UserPromptSubmittedHandler userPromptSubmittedHandler; +public class UserPromptSubmittedSignature { + UserPromptSubmittedHandler handler = (UserPromptSubmittedHookInput input, HookInvocation invocation) -> + CompletableFuture.completedFuture(null); + public static void main(String[] args) {} +} +``` + +```java +@FunctionalInterface +public interface UserPromptSubmittedHandler { + CompletableFuture handle( + UserPromptSubmittedHookInput input, + HookInvocation invocation); +} ``` {% endcodetab %} @@ -242,9 +256,11 @@ var session = await client.CreateSessionAsync(new SessionConfig {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.*; +import com.github.copilot.rpc.*; import java.util.concurrent.CompletableFuture; var hooks = new SessionHooks() diff --git a/content/copilot/how-tos/copilot-sdk/integrations/microsoft-agent-framework.md b/content/copilot/how-tos/copilot-sdk/integrations/microsoft-agent-framework.md index 2745d1516882..299ef507dc71 100644 --- a/content/copilot/how-tos/copilot-sdk/integrations/microsoft-agent-framework.md +++ b/content/copilot/how-tos/copilot-sdk/integrations/microsoft-agent-framework.md @@ -125,9 +125,8 @@ async def main(): ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(); client.start().get(); @@ -241,9 +240,8 @@ await session.sendAndWait({ prompt: "What's the weather like in Seattle?" }); {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -362,9 +360,8 @@ async def main(): ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; // Java uses the standard SDK directly — no MAF orchestrator needed var client = new CopilotClient(); @@ -438,9 +435,8 @@ Console.WriteLine(combinedResult); ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; import java.util.concurrent.CompletableFuture; // Java uses CompletableFuture for concurrent execution @@ -549,10 +545,11 @@ await session.sendAndWait({ prompt: "Write a quicksort implementation in TypeScr {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(); client.start().get(); diff --git a/content/copilot/how-tos/copilot-sdk/observability/opentelemetry.md b/content/copilot/how-tos/copilot-sdk/observability/opentelemetry.md index 6f5dbe8b5464..cd017a2892da 100644 --- a/content/copilot/how-tos/copilot-sdk/observability/opentelemetry.md +++ b/content/copilot/how-tos/copilot-sdk/observability/opentelemetry.md @@ -81,8 +81,8 @@ var client = new CopilotClient(new CopilotClientOptions ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(new CopilotClientOptions() .setTelemetry(new TelemetryConfig() @@ -123,6 +123,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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/streaming-events). + #### 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/content/copilot/how-tos/copilot-sdk/setup/backend-services.md b/content/copilot/how-tos/copilot-sdk/setup/backend-services.md index 325538a974b3..674a216528b5 100644 --- a/content/copilot/how-tos/copilot-sdk/setup/backend-services.md +++ b/content/copilot/how-tos/copilot-sdk/setup/backend-services.md @@ -20,7 +20,7 @@ contentType: how-tos ## How it works -Instead of the SDK spawning a CLI child process, you run the CLI independently in **headless server mode**. Your backend connects to it over TCP using the `Connection` option (`UriConnection`). +Instead of the SDK spawning a CLI child process, you run the CLI independently in **headless server mode**. Your backend connects to it over TCP using the `Connection` option (`URIConnection`). ![Diagram: Flowchart showing the described process.](/assets/images/help/copilot/copilot-sdk/setup-backend-services-diagram-0.png) @@ -30,6 +30,8 @@ Instead of the SDK spawning a CLI child process, you run the CLI independently i * 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy) for the full pattern. + ## Architecture: auto-managed vs. external CLI ![Diagram: Flowchart showing the described process.](/assets/images/help/copilot/copilot-sdk/setup-backend-services-diagram-1.png) @@ -103,15 +105,18 @@ Restart=always {% codetab 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", }); 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 }); @@ -153,9 +158,9 @@ func main() { userID := "user1" message := "Hello" - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.UriConnection{URL: "localhost:4321"}, - }) + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.URIConnection{URL: "localhost:4321"}, + }) client.Start(ctx) defer client.Stop() @@ -171,7 +176,7 @@ func main() { ```golang client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.UriConnection{URL: "localhost:4321"}, + Connection: copilot.URIConnection{URL: "localhost:4321"}, }) client.Start(ctx) defer client.Stop() @@ -228,9 +233,8 @@ var response = await session.SendAndWaitAsync( {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var userId = "user1"; var message = "Hello!"; @@ -277,17 +281,18 @@ copilot --headless --port 4321 Pass individual user tokens when creating sessions. See [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/github-oauth) for the full flow. ```typescript +const client = new CopilotClient({ + connection: RuntimeConnection.forUri("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({ @@ -304,7 +309,7 @@ Use your own API keys for the model provider. See [AUTOTITLE](/copilot/how-tos/c ```typescript const client = new CopilotClient({ - cliUrl: "localhost:4321", + connection: RuntimeConnection.forUri("localhost:4321"), }); const session = await client.createSession({ @@ -325,14 +330,15 @@ const session = await client.createSession({ ```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 +// 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", }); app.post("/api/chat", async (req, res) => { @@ -346,6 +352,8 @@ app.post("/api/chat", async (req, res) => { session = await client.createSession({ sessionId, model: "gpt-4.1", + availableTools: ["custom:*"], + gitHubToken: req.user.githubToken, }); } @@ -362,10 +370,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 @@ -468,11 +476,13 @@ setInterval(() => cleanupSessions(24 * 60 * 60 * 1000), 60 * 60 * 1000); | Need | Next Guide | |------|-----------| | Multiple CLI servers / high availability | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling) | +| SDK isolation for concurrent users | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy) | | GitHub account auth for users | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/github-oauth) | | Your own model keys | [AUTOTITLE](/copilot/how-tos/copilot-sdk/auth/byok) | ## Next steps +* **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy)**: Configure SDK isolation for concurrent users * **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling)**: Handle more users, add redundancy * **[AUTOTITLE](/copilot/how-tos/copilot-sdk/features/session-persistence)**: Resume sessions across restarts * **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/github-oauth)**: Add user authentication diff --git a/content/copilot/how-tos/copilot-sdk/setup/bundled-cli.md b/content/copilot/how-tos/copilot-sdk/setup/bundled-cli.md index 1be31c99ae6b..059cbd25ee8a 100644 --- a/content/copilot/how-tos/copilot-sdk/setup/bundled-cli.md +++ b/content/copilot/how-tos/copilot-sdk/setup/bundled-cli.md @@ -131,9 +131,8 @@ Console.WriteLine(response?.Data.Content); > The Java SDK does not bundle or embed the Copilot CLI. You must install the CLI separately and configure its path via `Connection` or the `COPILOT_CLI_PATH` environment variable. ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(new CopilotClientOptions() // Point to the CLI binary installed on the system diff --git a/content/copilot/how-tos/copilot-sdk/setup/choosing-a-setup-path.md b/content/copilot/how-tos/copilot-sdk/setup/choosing-a-setup-path.md index 04bace18a5fc..1fc8528d435e 100644 --- a/content/copilot/how-tos/copilot-sdk/setup/choosing-a-setup-path.md +++ b/content/copilot/how-tos/copilot-sdk/setup/choosing-a-setup-path.md @@ -43,6 +43,7 @@ You're building tools for your team or company. Users are employees who need to 1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/backend-services)**—Run the SDK in your internal services **If scaling beyond a single server:** +1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy)**—Configure SDK options for multi-user server mode 1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling)**—Handle multiple users and services ### 🚀 App developer (ISV) @@ -55,6 +56,7 @@ You're building a product for customers. You need to handle authentication for y 1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/backend-services)**—Power your product from server-side code **For production:** +1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy)**—Use `mode: "empty"`, per-session tokens, and isolated runtime state 1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling)**—Serve many customers reliably ### 🏗️ Platform developer @@ -63,6 +65,7 @@ You're embedding Copilot into a platform—APIs, developer tools, or infrastruct **Start with:** 1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/backend-services)**—Core server-side integration +1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy)**—SDK-level isolation, per-session auth, and shared runtime options 1. **[AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling)**—Session isolation, horizontal scaling, persistence **Depending on your auth model:** @@ -81,6 +84,7 @@ Use this table to find the right guides based on what you need to do: | Use your own model keys (OpenAI, Azure, etc.) | [AUTOTITLE](/copilot/how-tos/copilot-sdk/auth/byok) | | Azure BYOK with Managed Identity (no API keys) | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/azure-managed-identity) | | Run the SDK on a server | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/backend-services) | +| Configure SDK options for concurrent users | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy) | | Serve multiple users / scale horizontally | [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling) | ## Configuration comparison diff --git a/content/copilot/how-tos/copilot-sdk/setup/github-oauth.md b/content/copilot/how-tos/copilot-sdk/setup/github-oauth.md index 99189359dc3b..ddb3c29df73b 100644 --- a/content/copilot/how-tos/copilot-sdk/setup/github-oauth.md +++ b/content/copilot/how-tos/copilot-sdk/setup/github-oauth.md @@ -162,7 +162,7 @@ func main() { ```golang func createClientForUser(userToken string) *copilot.Client { return copilot.NewClient(&copilot.ClientOptions{ - GithubToken: userToken, + GitHubToken: userToken, UseLoggedInUser: copilot.Bool(false), }) } @@ -228,10 +228,11 @@ var response = await session.SendAndWaitAsync( {% endcodetab %} {% codetab java %} + + ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.*; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; CopilotClient createClientForUser(String userToken) throws Exception { var client = new CopilotClient(new CopilotClientOptions() diff --git a/content/copilot/how-tos/copilot-sdk/setup/index.md b/content/copilot/how-tos/copilot-sdk/setup/index.md index 71dac28cdc25..1766006906b2 100644 --- a/content/copilot/how-tos/copilot-sdk/setup/index.md +++ b/content/copilot/how-tos/copilot-sdk/setup/index.md @@ -14,6 +14,7 @@ children: - /choosing-a-setup-path - /github-oauth - /local-cli + - /multi-tenancy - /scaling --- diff --git a/content/copilot/how-tos/copilot-sdk/setup/multi-tenancy.md b/content/copilot/how-tos/copilot-sdk/setup/multi-tenancy.md new file mode 100644 index 000000000000..3d6e4aedccc0 --- /dev/null +++ b/content/copilot/how-tos/copilot-sdk/setup/multi-tenancy.md @@ -0,0 +1,440 @@ +--- +title: Multi-tenancy and server deployments +shortTitle: Multi Tenancy +intro: >- + Run the Copilot SDK in multi-user server deployments with per-session + isolation for state, authentication, and tools. +versions: + fpt: '*' + ghec: '*' +contentType: how-tos +--- + + + + +**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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling). 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. + +{% codetabs %} +{% codetab typescript %} + +```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", + 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, +}); +``` + +{% endcodetab %} +{% codetab 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, +) +``` + +{% endcodetab %} +{% codetab go %} + +```golang +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: "YOUR_GITHUB_TOKEN"} + + 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 +} +``` + +```golang +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, +}) +``` + +{% endcodetab %} +{% codetab dotnet %} + +```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 = "YOUR_GITHUB_TOKEN" }; + +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 +{ + 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, +}); +``` + +{% endcodetab %} +{% codetab java %} + +```java +import java.util.List; +import com.github.copilot.CopilotClient; +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() + .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(); +``` + +{% endcodetab %} +{% codetab 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?; +``` + +{% endcodetab %} +{% endcodetabs %} + +### `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: { + initialCwd: "/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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/session-persistence) 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)` | +| 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling). | +| 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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/cloud-sessions). | + +### 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. + +![Diagram: Flowchart showing the described process.](/assets/images/help/copilot/copilot-sdk/setup-multi-tenancy-diagram-0.png) + +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 + +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/scaling): deployment topologies, storage patterns, and isolation comparisons +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/backend-services): running the runtime in headless server mode +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/auth/byok): using your own model provider credentials +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/cloud-sessions): routing selected work to cloud sessions +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/session-persistence): managing resumable session state +* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features): tools, events, hooks, and advanced SDK features diff --git a/content/copilot/how-tos/copilot-sdk/setup/scaling.md b/content/copilot/how-tos/copilot-sdk/setup/scaling.md index 7f66293d26c6..d45441808b01 100644 --- a/content/copilot/how-tos/copilot-sdk/setup/scaling.md +++ b/content/copilot/how-tos/copilot-sdk/setup/scaling.md @@ -14,6 +14,8 @@ contentType: how-tos +For SDK-level options and patterns, see [AUTOTITLE](/copilot/how-tos/copilot-sdk/setup/multi-tenancy). + **Best for:** Platform developers, SaaS builders, any deployment serving more than a handful of concurrent users. ## Core concepts diff --git a/content/copilot/how-tos/copilot-sdk/troubleshooting/compatibility.md b/content/copilot/how-tos/copilot-sdk/troubleshooting/compatibility.md index 4315e8b0dd69..60cd2712584f 100644 --- a/content/copilot/how-tos/copilot-sdk/troubleshooting/compatibility.md +++ b/content/copilot/how-tos/copilot-sdk/troubleshooting/compatibility.md @@ -97,7 +97,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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/fleet-mode) | | 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 | @@ -181,6 +181,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 [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/fleet-mode). + ### Session export The `--share` option is not available via SDK. Workarounds: diff --git a/content/copilot/how-tos/copilot-sdk/troubleshooting/debugging.md b/content/copilot/how-tos/copilot-sdk/troubleshooting/debugging.md index ccfb2e783a38..301053a71553 100644 --- a/content/copilot/how-tos/copilot-sdk/troubleshooting/debugging.md +++ b/content/copilot/how-tos/copilot-sdk/troubleshooting/debugging.md @@ -99,8 +99,8 @@ var client = new CopilotClient(new CopilotClientOptions {% codetab java %} ```java -import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.json.*; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.*; var client = new CopilotClient(new CopilotClientOptions() .setLogLevel("debug") @@ -174,6 +174,8 @@ var client = new CopilotClient(new CopilotClientOptions {% endcodetab %} {% codetab java %} + + ```java // The Java SDK does not currently support passing extra CLI arguments. // For custom log directories, run the CLI manually with --log-dir @@ -284,7 +286,7 @@ var client = new CopilotClient(new CopilotClientOptions ```golang client := copilot.NewClient(&copilot.ClientOptions{ - GithubToken: os.Getenv("GITHUB_TOKEN"), + GitHubToken: os.Getenv("GITHUB_TOKEN"), }) ``` @@ -294,7 +296,7 @@ var client = new CopilotClient(new CopilotClientOptions ```csharp var client = new CopilotClient(new CopilotClientOptions { - GithubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN") + GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN") }); ``` diff --git a/content/copilot/how-tos/copilot-sdk/troubleshooting/mcp-debugging.md b/content/copilot/how-tos/copilot-sdk/troubleshooting/mcp-debugging.md index 506efb703cc7..e6e751f7df2c 100644 --- a/content/copilot/how-tos/copilot-sdk/troubleshooting/mcp-debugging.md +++ b/content/copilot/how-tos/copilot-sdk/troubleshooting/mcp-debugging.md @@ -423,7 +423,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/content/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-via-the-api.md b/content/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-via-the-api.md index a9de498934ca..fe5ff23625ef 100644 --- a/content/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-via-the-api.md +++ b/content/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-via-the-api.md @@ -1,8 +1,8 @@ --- title: Using Copilot cloud agent via the API shortTitle: Use cloud agent via the API -intro: 'You can start and manage {% data variables.copilot.copilot_cloud_agent %} tasks directly using the REST API and assign issues to Copilot using the issues REST and GraphQL APIs.' -product: '{% data reusables.gated-features.copilot-business-and-enterprise %}' +intro: 'You can start and manage {% data variables.copilot.copilot_cloud_agent %} tasks programmatically using the REST API.' +product: '{% data reusables.gated-features.copilot-cloud-agent %}
Sign up for {% data variables.product.prodname_copilot_short %} {% octicon "link-external" height:16 %}' versions: feature: copilot contentType: how-tos diff --git a/content/copilot/reference/copilot-usage-metrics/team-level-metrics.md b/content/copilot/reference/copilot-usage-metrics/team-level-metrics.md index c44fa7ca11e7..15246925fd28 100644 --- a/content/copilot/reference/copilot-usage-metrics/team-level-metrics.md +++ b/content/copilot/reference/copilot-usage-metrics/team-level-metrics.md @@ -19,7 +19,7 @@ The same join recipe supports any team-level slice you need: per `(team, day)`, ## Fetching the reports -The two reports referenced in this guide are downloaded in two steps. First, call the REST endpoint for the day you want. The endpoint returns time-limited signed URLs from which you can download the report files. Then download the JSON files those URLs point to. The user-team and per-user rows are in those JSON files; they are not returned inline by the REST endpoint. +The two reports referenced in this guide are downloaded in two steps. First, call the REST endpoint for the day you want. The endpoint returns time-limited signed URLs from which you can download the report files. Then download the newline-delimited JSON (NDJSON) files those URLs point to. The user-teams and per-user rows are in those NDJSON files; they are not returned inline by the REST endpoint. | Report | Endpoint | |:--|:--| @@ -33,7 +33,7 @@ Each endpoint returns a response of the form: ```json { "download_links": [ - "https://example.com/copilot-user-teams-report-1.json" + "https://example.com/copilot-user-teams-report-1.ndjson" ], "report_day": "2026-05-07" } @@ -89,7 +89,7 @@ The entity-level reports (`enterprise_28_day`, `organization_28_day`, `enterpris ## Example -This minimal end-to-end example produces one day of organization-team metrics. The JSON shown below for each input report is a sample of the rows you would find in the file downloaded from one of that report's `download_links` (see [Fetching the reports](#fetching-the-reports) above). +This minimal end-to-end example produces one day of organization-team metrics. The sample NDJSON rows below for each input report are like the rows you would find in the file downloaded from one of that report's `download_links` (see [Fetching the reports](#fetching-the-reports) above). Two users have {% data variables.product.prodname_copilot_short %} activity on 2026-05-07 in organization `999`: diff --git a/content/copilot/tutorials/roll-out-at-scale/measure-success.md b/content/copilot/tutorials/roll-out-at-scale/measure-success.md index 2d22f6ebcbdf..2b184b52e89a 100644 --- a/content/copilot/tutorials/roll-out-at-scale/measure-success.md +++ b/content/copilot/tutorials/roll-out-at-scale/measure-success.md @@ -100,7 +100,7 @@ Example response: ```json { "download_links": [ - "https://example.com/copilot-usage-report.json" + "https://example.com/copilot-usage-report.ndjson" ], "report_start_day": "2025-07-18", "report_end_day": "2025-08-14" diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index 9fe44393fc2c..a933f4951dfa 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -544,6 +544,9 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{%- sugestões embutidas do variables.', '{%- data variables.') // Fully translated reusables path: `{% dados reutilizáveis.X.Y %}` → `{% data reusables.X.Y %}` content = content.replaceAll('{% dados reutilizáveis.', '{% data reusables.') + // `{% dado reutilizáveis.X.Y %}` — singular "dado" (datum) + plural "reutilizáveis" + content = content.replaceAll('{% dado reutilizáveis.', '{% data reusables.') + content = content.replaceAll('{%- dado reutilizáveis.', '{%- data reusables.') // Translated path segment inside reusables path: `repositórios` → `repositories` content = content.replaceAll( '{% data reusables.repositórios.', @@ -566,6 +569,11 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% variáveis de dados ', '{% data variables ') // `{% dados variáveis.` — alternate word order "data variables" content = content.replaceAll('{% dados variáveis.', '{% data variables.') + // `{% Espaços de Código %}` / `{% espaços de código %}` — "Code Spaces" = codespaces + content = content.replaceAll('{% Espaços de Código %}', '{% codespaces %}') + content = content.replaceAll('{%- Espaços de Código %}', '{%- codespaces %}') + content = content.replaceAll('{% espaços de código %}', '{% codespaces %}') + content = content.replaceAll('{%- espaços de código %}', '{%- codespaces %}') // `{% janelas %}` — Portuguese "windows" = windows (platform tag) content = content.replaceAll('{% janelas %}', '{% windows %}') content = content.replaceAll('{%- janelas %}', '{%- windows %}') @@ -675,6 +683,9 @@ export function correctTranslatedContentStrings( // `{% caso contrário %}` — alternate "otherwise" = else content = content.replaceAll('{% caso contrário %}', '{% else %}') content = content.replaceAll('{%- caso contrário %}', '{%- else %}') + // `{% outra %}` — "other/another" (feminine) = else + content = content.replaceAll('{% outra %}', '{% else %}') + content = content.replaceAll('{%- outra %}', '{%- else %}') // `{% observação %}` — "note" = note content = content.replaceAll('{% observação %}', '{% note %}') content = content.replaceAll('{%- observação %}', '{%- note %}') @@ -1152,6 +1163,9 @@ export function correctTranslatedContentStrings( } if (context.code === 'fr') { + // `{% espaces de code %}` — French "code spaces" = codespaces + content = content.replaceAll('{% espaces de code %}', '{% codespaces %}') + content = content.replaceAll('{%- espaces de code %}', '{%- codespaces %}') // `{% sinon %}` — "otherwise" = else content = content.replaceAll('{% sinon %}', '{% else %}') content = content.replaceAll('{%- sinon %}', '{%- else %}') @@ -1639,6 +1653,8 @@ export function correctTranslatedContentStrings( '{%$1data reusables.', ) content = content.replace(/\{%(-?\s*)data Variablen\./g, '{%$1data variables.') + // `data variablen.` — lowercase variant of "Variablen" (survives after broad fallback) + content = content.replace(/\{%(-?\s*)data variablen\./g, '{%$1data variables.') // German `oder` = "or", `und` = "and" inside ifversion/elsif/if tags content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?\soder\s[^%]*?-?%\}/g, (m) => m.replace(/\soder\s/g, ' or '),