Skip to content

Commit 3bcca2a

Browse files
Add VS Code launch config for debugging SDK/runtime (#925)
* Add VS Code launch config for debugging sdk/runtime * Cleanup * Make all SDKs consistent in support for COPILOT_CLI_PATH * Respect uv lockfile to avoid getting new ty versions randomly * Clean up Node readme * Update python-sdk-tests.yml * Different fix for random ty versioning
1 parent 5b58582 commit 3bcca2a

File tree

10 files changed

+110
-47
lines changed

10 files changed

+110
-47
lines changed

.vscode/launch.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Debug Node.js SDK (chat sample)",
6+
"type": "node",
7+
"request": "launch",
8+
"runtimeArgs": ["--enable-source-maps", "--import", "tsx"],
9+
"program": "samples/chat.ts",
10+
"cwd": "${workspaceFolder}/nodejs",
11+
"env": {
12+
"COPILOT_CLI_PATH": "${workspaceFolder}/../copilot-agent-runtime/dist-cli/index.js"
13+
},
14+
"console": "integratedTerminal",
15+
"autoAttachChildProcesses": true,
16+
"sourceMaps": true,
17+
"resolveSourceMapLocations": [
18+
"${workspaceFolder}/**",
19+
"${workspaceFolder}/../copilot-agent-runtime/**"
20+
]
21+
}
22+
]
23+
}

dotnet/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ new CopilotClient(CopilotClientOptions? options = null)
6767

6868
**Options:**
6969

70-
- `CliPath` - Path to CLI executable (default: "copilot" from PATH)
70+
- `CliPath` - Path to CLI executable (default: `COPILOT_CLI_PATH` env var, or bundled CLI)
7171
- `CliArgs` - Extra arguments prepended before SDK-managed flags
7272
- `CliUrl` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`). When provided, the client will not spawn a CLI process.
7373
- `Port` - Server port (default: 0 for random)

dotnet/src/Client.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,8 +1064,12 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
10641064

10651065
private static async Task<(Process Process, int? DetectedLocalhostTcpPort, StringBuilder StderrBuffer)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken)
10661066
{
1067-
// Use explicit path or bundled CLI - no PATH fallback
1068-
var cliPath = options.CliPath ?? GetBundledCliPath(out var searchedPath)
1067+
// Use explicit path, COPILOT_CLI_PATH env var (from options.Environment or process env), or bundled CLI - no PATH fallback
1068+
var envCliPath = options.Environment is not null && options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) ? envValue
1069+
: System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
1070+
var cliPath = options.CliPath
1071+
?? envCliPath
1072+
?? GetBundledCliPath(out var searchedPath)
10691073
?? throw new InvalidOperationException($"Copilot CLI not found at '{searchedPath}'. Ensure the SDK NuGet package was restored correctly or provide an explicit CliPath.");
10701074
var args = new List<string>();
10711075

go/client.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,29 @@ func NewClient(options *ClientOptions) *Client {
199199
opts.Env = os.Environ()
200200
}
201201

202-
// Check environment variable for CLI path
203-
if cliPath := os.Getenv("COPILOT_CLI_PATH"); cliPath != "" {
204-
opts.CLIPath = cliPath
202+
// Check effective environment for CLI path (only if not explicitly set via options)
203+
if opts.CLIPath == "" {
204+
if cliPath := getEnvValue(opts.Env, "COPILOT_CLI_PATH"); cliPath != "" {
205+
opts.CLIPath = cliPath
206+
}
205207
}
206208

207209
client.options = opts
208210
return client
209211
}
210212

213+
// getEnvValue looks up a key in an environment slice ([]string of "KEY=VALUE").
214+
// Returns the value if found, or empty string otherwise.
215+
func getEnvValue(env []string, key string) string {
216+
prefix := key + "="
217+
for i := len(env) - 1; i >= 0; i-- {
218+
if strings.HasPrefix(env[i], prefix) {
219+
return env[i][len(prefix):]
220+
}
221+
}
222+
return ""
223+
}
224+
211225
// parseCliUrl parses a CLI URL into host and port components.
212226
//
213227
// Supports formats: "host:port", "http://host:port", "https://host:port", or just "port".

nodejs/README.md

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ await client.stop();
6060
Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+/Node.js 18.0+):
6161

6262
```typescript
63-
await using session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll });
63+
await using session = await client.createSession({
64+
model: "gpt-5",
65+
onPermissionRequest: approveAll,
66+
});
6467
// session is automatically disconnected when leaving scope
6568
```
6669

@@ -76,7 +79,7 @@ new CopilotClient(options?: CopilotClientOptions)
7679

7780
**Options:**
7881

79-
- `cliPath?: string` - Path to CLI executable (default: "copilot" from PATH)
82+
- `cliPath?: string` - Path to CLI executable (default: uses COPILOT_CLI_PATH env var or bundled instance)
8083
- `cliArgs?: string[]` - Extra arguments prepended before SDK-managed flags (e.g. `["./dist-cli/index.js"]` when using `node`)
8184
- `cliUrl?: string` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process.
8285
- `port?: number` - Server port (default: 0 for random)
@@ -184,6 +187,7 @@ const unsubscribe = client.on((event) => {
184187
```
185188

186189
**Lifecycle Event Types:**
190+
187191
- `session.created` - A new session was created
188192
- `session.deleted` - A session was deleted
189193
- `session.updated` - A session was updated (e.g., new messages)
@@ -293,7 +297,7 @@ if (session.capabilities.ui?.elicitation) {
293297

294298
Interactive UI methods for showing dialogs to the user. Only available when the CLI host supports elicitation (`session.capabilities.ui?.elicitation === true`). See [UI Elicitation](#ui-elicitation) for full details.
295299

296-
##### `destroy(): Promise<void>` *(deprecated)*
300+
##### `destroy(): Promise<void>` _(deprecated)_
297301

298302
Deprecated — use `disconnect()` instead.
299303

@@ -454,8 +458,10 @@ defineTool("edit_file", {
454458
description: "Custom file editor with project-specific validation",
455459
parameters: z.object({ path: z.string(), content: z.string() }),
456460
overridesBuiltInTool: true,
457-
handler: async ({ path, content }) => { /* your logic */ },
458-
})
461+
handler: async ({ path, content }) => {
462+
/* your logic */
463+
},
464+
});
459465
```
460466

461467
#### Skipping Permission Prompts
@@ -467,8 +473,10 @@ defineTool("safe_lookup", {
467473
description: "A read-only lookup that needs no confirmation",
468474
parameters: z.object({ id: z.string() }),
469475
skipPermission: true,
470-
handler: async ({ id }) => { /* your logic */ },
471-
})
476+
handler: async ({ id }) => {
477+
/* your logic */
478+
},
479+
});
472480
```
473481

474482
### Commands
@@ -571,7 +579,10 @@ const session = await client.createSession({
571579
mode: "customize",
572580
sections: {
573581
// Replace the tone/style section
574-
tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
582+
tone: {
583+
action: "replace",
584+
content: "Respond in a warm, professional tone. Be thorough in explanations.",
585+
},
575586
// Remove coding-specific rules
576587
code_change_rules: { action: "remove" },
577588
// Append to existing guidelines
@@ -586,6 +597,7 @@ const session = await client.createSession({
586597
Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. Use the `SYSTEM_PROMPT_SECTIONS` constant for descriptions of each section.
587598

588599
Each section override supports four actions:
600+
589601
- **`replace`** — Replace the section content entirely
590602
- **`remove`** — Remove the section from the prompt
591603
- **`append`** — Add content after the existing section
@@ -624,7 +636,7 @@ const session = await client.createSession({
624636
model: "gpt-5",
625637
infiniteSessions: {
626638
enabled: true,
627-
backgroundCompactionThreshold: 0.80, // Start compacting at 80% context usage
639+
backgroundCompactionThreshold: 0.8, // Start compacting at 80% context usage
628640
bufferExhaustionThreshold: 0.95, // Block at 95% until compaction completes
629641
},
630642
});
@@ -723,8 +735,8 @@ const session = await client.createSession({
723735
const session = await client.createSession({
724736
model: "gpt-4",
725737
provider: {
726-
type: "azure", // Must be "azure" for Azure endpoints, NOT "openai"
727-
baseUrl: "https://my-resource.openai.azure.com", // Just the host, no path
738+
type: "azure", // Must be "azure" for Azure endpoints, NOT "openai"
739+
baseUrl: "https://my-resource.openai.azure.com", // Just the host, no path
728740
apiKey: process.env.AZURE_OPENAI_KEY,
729741
azure: {
730742
apiVersion: "2024-10-21",
@@ -734,6 +746,7 @@ const session = await client.createSession({
734746
```
735747

736748
> **Important notes:**
749+
>
737750
> - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified.
738751
> - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`.
739752
> - The `baseUrl` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically.
@@ -744,9 +757,9 @@ The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` co
744757

745758
```typescript
746759
const client = new CopilotClient({
747-
telemetry: {
748-
otlpEndpoint: "http://localhost:4318",
749-
},
760+
telemetry: {
761+
otlpEndpoint: "http://localhost:4318",
762+
},
750763
});
751764
```
752765

@@ -772,12 +785,12 @@ If you're already using `@opentelemetry/api` in your app and want this linkage,
772785
import { propagation, context } from "@opentelemetry/api";
773786

774787
const client = new CopilotClient({
775-
telemetry: { otlpEndpoint: "http://localhost:4318" },
776-
onGetTraceContext: () => {
777-
const carrier: Record<string, string> = {};
778-
propagation.inject(context.active(), carrier);
779-
return carrier;
780-
},
788+
telemetry: { otlpEndpoint: "http://localhost:4318" },
789+
onGetTraceContext: () => {
790+
const carrier: Record<string, string> = {};
791+
propagation.inject(context.active(), carrier);
792+
return carrier;
793+
},
781794
});
782795
```
783796

@@ -837,14 +850,15 @@ const session = await client.createSession({
837850

838851
### Permission Result Kinds
839852

840-
| Kind | Meaning |
841-
|------|---------|
842-
| `"approved"` | Allow the tool to run |
843-
| `"denied-interactively-by-user"` | User explicitly denied the request |
844-
| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked |
845-
| `"denied-by-rules"` | Denied by a policy rule |
846-
| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy |
847-
| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) |
853+
| Kind | Meaning |
854+
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
855+
| `"approved"` | Allow the tool to run |
856+
| `"denied-interactively-by-user"` | User explicitly denied the request |
857+
| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked |
858+
| `"denied-by-rules"` | Denied by a policy rule |
859+
| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy |
860+
| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) |
861+
848862
### Resuming Sessions
849863

850864
Pass `onPermissionRequest` when resuming a session too — it is required:

nodejs/samples/chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as readline from "node:readline";
21
import { CopilotClient, approveAll, type SessionEvent } from "@github/copilot-sdk";
2+
import * as readline from "node:readline";
33

44
async function main() {
55
const client = new CopilotClient();

nodejs/src/client.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,11 @@ export class CopilotClient {
308308
this.onListModels = options.onListModels;
309309
this.onGetTraceContext = options.onGetTraceContext;
310310

311+
const effectiveEnv = options.env ?? process.env;
311312
this.options = {
312-
cliPath: options.cliUrl ? undefined : options.cliPath || getBundledCliPath(),
313+
cliPath: options.cliUrl
314+
? undefined
315+
: options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
313316
cliArgs: options.cliArgs ?? [],
314317
cwd: options.cwd ?? process.cwd(),
315318
port: options.port || 0,
@@ -320,7 +323,7 @@ export class CopilotClient {
320323
autoStart: options.autoStart ?? true,
321324
autoRestart: false,
322325

323-
env: options.env ?? process.env,
326+
env: effectiveEnv,
324327
githubToken: options.githubToken,
325328
// Default useLoggedInUser to false when githubToken is provided, otherwise true
326329
useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),

python/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ CopilotClient(
125125

126126
**SubprocessConfig** — spawn a local CLI process:
127127

128-
- `cli_path` (str | None): Path to CLI executable (default: bundled binary)
128+
- `cli_path` (str | None): Path to CLI executable (default: `COPILOT_CLI_PATH` env var, or bundled binary)
129129
- `cli_args` (list[str]): Extra arguments for the CLI executable
130130
- `cwd` (str | None): Working directory for CLI process (default: current dir)
131131
- `use_stdio` (bool): Use stdio transport instead of TCP (default: True)

python/copilot/client.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -794,16 +794,21 @@ def __init__(
794794
else:
795795
self._actual_port = None
796796

797-
# Resolve CLI path: explicit > bundled binary
797+
# Resolve CLI path: explicit > COPILOT_CLI_PATH env var > bundled binary
798+
effective_env = config.env if config.env is not None else os.environ
798799
if config.cli_path is None:
799-
bundled_path = _get_bundled_cli_path()
800-
if bundled_path:
801-
config.cli_path = bundled_path
800+
env_cli_path = effective_env.get("COPILOT_CLI_PATH")
801+
if env_cli_path:
802+
config.cli_path = env_cli_path
802803
else:
803-
raise RuntimeError(
804-
"Copilot CLI not found. The bundled CLI binary is not available. "
805-
"Ensure you installed a platform-specific wheel, or provide cli_path."
806-
)
804+
bundled_path = _get_bundled_cli_path()
805+
if bundled_path:
806+
config.cli_path = bundled_path
807+
else:
808+
raise RuntimeError(
809+
"Copilot CLI not found. The bundled CLI binary is not available. "
810+
"Ensure you installed a platform-specific wheel, or provide cli_path."
811+
)
807812

808813
# Resolve use_logged_in_user default
809814
if config.use_logged_in_user is None:

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Repository = "https://github.com/github/copilot-sdk"
3434
[project.optional-dependencies]
3535
dev = [
3636
"ruff>=0.1.0",
37-
"ty>=0.0.2",
37+
"ty>=0.0.2,<0.0.25",
3838
"pytest>=7.0.0",
3939
"pytest-asyncio>=0.21.0",
4040
"pytest-timeout>=2.0.0",

0 commit comments

Comments
 (0)