Skip to content

Add minimal chat samples for all SDK languages#492

Merged
SteveSandersonMS merged 18 commits intomainfrom
stevesa/add-samples
Feb 17, 2026
Merged

Add minimal chat samples for all SDK languages#492
SteveSandersonMS merged 18 commits intomainfrom
stevesa/add-samples

Conversation

@SteveSandersonMS
Copy link
Contributor

@SteveSandersonMS SteveSandersonMS commented Feb 17, 2026

This PR adds minimal interactive chat samples for each SDK language (Node.js, Python, Go, .NET).

While doing this it became clear some other fundamentals needed to be fixed:

  • Not all the SDKs supported passing CLI args (fixed)
  • None of the SDKs had proper error handling in the case where the CLI terminates unexpectedly (fixed: now all subsequent RPC interactions will fail cleanly, and any ones that were pending at the time of the CLI termination will fail immediately and get an error that contains any stderr output written by the underlying CLI)
    • In particular this means if you try to use a CLI that terminates immediately on startup without even starting the server (e.g., unsupported CLI args), you'll now get a clear error back from the "start" call, whereas before it would just hang

Changes

  • nodejs/samples/chat.ts - TypeScript chat sample with streaming
  • python/samples/chat.py - Python async chat sample
  • go/samples/chat.go - Go chat sample
  • dotnet/samples/Chat.cs - .NET top-level chat sample

Each sample is ~30-50 lines and demonstrates:

  • Creating a client and session with default options
  • Subscribing to events and logging deltas/messages
  • Interactive prompt loop using sendAndWait

Also updates each language README with a Run the Sample section.

Testing

Verified builds compile:

  • dotnet build dotnet/samples
  • go build go/samples- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner February 17, 2026 11:24
Copilot AI review requested due to automatic review settings February 17, 2026 11:24
@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/add-samples branch 3 times, most recently from ced2b4f to f7f81c2 Compare February 17, 2026 11:29
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds minimal interactive “chat” samples for each language SDK in this mono-repo, plus README sections that explain how to run them. This fits the repo’s goal of providing SDK entry points that talk to the Copilot CLI via JSON-RPC by giving users a copy/paste starting point in each language.

Changes:

  • Add interactive chat samples for Node.js, Python, Go, and .NET.
  • Add “Run the Sample” sections to each language README.
  • Add Node and Go sample module scaffolding (Node package.json, Go go.mod) to run samples standalone.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
python/samples/chat.py New async chat loop sample (currently has API/typing mismatches and output issues).
python/README.md Adds “Run the Sample” instructions for Python.
nodejs/samples/package.json New sample package scaffolding (currently depends on built dist/ which isn’t present).
nodejs/samples/chat.ts New TS chat loop sample (currently doesn’t enable streaming and uses wrong delta field name).
nodejs/README.md Adds “Run the Sample” instructions for Node (currently missing required build step / approach).
go/samples/go.mod New Go sample module with replace to local SDK.
go/samples/chat.go New Go chat loop sample (currently ignores returned errors and doesn’t enable streaming).
go/README.md Adds “Run the Sample” instructions for Go.
dotnet/samples/Chat.csproj New .NET sample project referencing SDK project.
dotnet/samples/Chat.cs New .NET top-level chat loop sample (currently doesn’t enable streaming; EOF handling/output issues).
dotnet/README.md Adds “Run the Sample” instructions for .NET.
Comments suppressed due to low confidence (4)

go/samples/chat.go:25

  • session.Destroy() returns an error; deferring it without checking can hide failures (and also makes debugging harder if destroy fails). Consider handling/logging the error in the deferred call.
	defer session.Destroy()

go/samples/chat.go:50

  • SendAndWait returns (*SessionEvent, error). The current call ignores the error, so failures (including timeouts / session errors) will be missed and the prompt loop will continue as if nothing happened. Capture and handle the error (and optionally use the returned final message).
	}
}

nodejs/samples/chat.ts:31

  • There’s no graceful shutdown path (session destroy / client stop / readline close). Consider handling SIGINT/EOF to cleanly await session.destroy(); await client.stop(); rl.close(); before exiting, since the client may spawn a CLI subprocess.
main().catch(console.error);

dotnet/samples/Chat.cs:24

  • The loop prints an "Assistant: " prefix before calling SendAndWaitAsync, but the event handler also prints a \nAssistant: ... line for AssistantMessageEvent. This produces duplicated/garbled output; prefer letting the handler own assistant output (remove the pre-send prefix) or only printing deltas during the call.
    Console.WriteLine();
}

Comment on lines 19 to 25
defer client.Stop()

session, err := client.CreateSession(ctx, &copilot.SessionConfig{Streaming: true})
if err != nil {
panic(err)
}
defer session.Destroy()
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.Stop() returns an error (it can aggregate cleanup errors). Deferring it without checking means cleanup failures are silently lost; wrap the defer in a function and handle/log the returned error.

This issue also appears in the following locations of the same file:

  • line 25
  • line 49
Suggested change
defer client.Stop()
session, err := client.CreateSession(ctx, &copilot.SessionConfig{Streaming: true})
if err != nil {
panic(err)
}
defer session.Destroy()
defer func() {
if err := client.Stop(); err != nil {
fmt.Fprintln(os.Stderr, "failed to stop Copilot client:", err)
}
}()
session, err := client.CreateSession(ctx, &copilot.SessionConfig{Streaming: true})
if err != nil {
panic(err)
}
defer func() {
if err := session.Destroy(); err != nil {
fmt.Fprintln(os.Stderr, "failed to destroy Copilot session:", err)
}
}()

Copilot uses AI. Check for mistakes.
Comment on lines 6 to 7
async with CopilotClient() as client:
session = await client.create_session({"streaming": True})
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopilotClient does not implement an async context manager (aenter/aexit), so async with CopilotClient() will raise at runtime. Use client = CopilotClient(); await client.start() and ensure await client.stop() is called in a finally block (and destroy the session as well).

Copilot uses AI. Check for mistakes.
Comment on lines 6 to 8
async with CopilotClient() as client:
session = await client.create_session({"streaming": True})

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sample expects assistant.message_delta events, but streaming is opt-in; create the session with {"streaming": True} (or equivalent) so the delta branch can ever run.

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 31
if event.Type == copilot.AssistantMessageDelta && event.Data.DeltaContent != nil {
fmt.Print(*event.Data.DeltaContent)
}
})
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop prints an "Assistant: " prefix before sending, but the event handler also prints a full assistant line on AssistantMessage. This produces duplicated/garbled output (and duplicates content if deltas are printed). Prefer either printing only deltas and using the final message event just to end the line, or letting the handler print the final message and removing the pre-send prefix.

Copilot uses AI. Check for mistakes.
using GitHub.Copilot.SDK;

await using var client = new CopilotClient();
await using var session = await client.CreateSessionAsync(new SessionConfig { Streaming = true });
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample prints AssistantMessageDeltaEvent, but streaming is disabled by default. Create the session with a SessionConfig { Streaming = true } so delta events are actually emitted.

Copilot uses AI. Check for mistakes.
Comment on lines 8 to 11
if (evt is AssistantMessageDeltaEvent delta)
Console.Write(delta.Data?.DeltaContent);
});

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If streaming deltas are printed, printing the full AssistantMessageEvent content afterwards will duplicate the response. Consider ignoring the final content when streaming (or accumulate deltas and print once).

Copilot uses AI. Check for mistakes.
Comment on lines 10 to 13
if event.type == "assistant.message_delta":
print(event.data.delta, end="", flush=True)

session.on(on_event)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event.type is a SessionEventType enum (not a string), and the delta field is event.data.delta_content (not delta). Update the comparisons to use SessionEventType.ASSISTANT_MESSAGE / .ASSISTANT_MESSAGE_DELTA (or compare event.type.value) and print event.data.delta_content.

Copilot uses AI. Check for mistakes.
Comment on lines 10 to 12
if event.type == "assistant.message_delta":
print(event.data.delta, end="", flush=True)

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When streaming is enabled, assistant.message_delta events arrive followed by a final assistant.message. Printing the full assistant.message content after printing deltas will duplicate the response. Consider ignoring assistant.message content when streaming (or accumulate deltas and print once).

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 7
const client = new CopilotClient();
const session = await client.createSession({ streaming: true });

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample handles assistant.message_delta, but streaming is disabled by default. Create the session with { streaming: true } (e.g., await client.createSession({ streaming: true })) so delta events are actually emitted.

Copilot uses AI. Check for mistakes.

session.on((event: SessionEvent) => {
if (event.type === "assistant.message_delta") {
process.stdout.write(event.data?.delta ?? "");
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For assistant.message_delta events the field is event.data.deltaContent (see generated session event types), not event.data.delta. Using delta will always print empty output.

Suggested change
process.stdout.write(event.data?.delta ?? "");
process.stdout.write(event.data?.deltaContent ?? "");

Copilot uses AI. Check for mistakes.


async def main():
async with CopilotClient() as client:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: The Python SDK does not implement async context manager (__aenter__ and __aexit__ methods), so this async with syntax will fail with AttributeError: __aenter__.

The Python sample should follow the explicit lifecycle pattern like Go:

async def main():
    client = CopilotClient()
    await client.start()
    try:
        session = await client.create_session({"streaming": True})
        # ... rest of code
    finally:
        await client.stop()

Cross-SDK Inconsistency: This reveals different lifecycle management patterns across SDKs:

  • .NET: Uses await using for automatic disposal ✅
  • Go: Explicitly calls Start()/Stop() with defer
  • Python: Should use explicit start()/stop() (context manager not implemented)
  • Node.js: No cleanup shown (potential resource leak)

AI generated by SDK Consistency Review Agent for #492


def on_event(event):
if event.type == "assistant.message_delta":
print(event.data.delta, end="", flush=True)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The field name should be delta_content (snake_case), not delta.

According to python/copilot/generated/session_events.py:736, the field is named delta_content:

print(event.data.delta_content, end="", flush=True)

Cross-SDK Note: Field names follow language conventions:

  • Python: event.data.delta_content (snake_case)
  • Node.js: event.data.deltaContent (camelCase)
  • Go: event.Data.DeltaContent (PascalCase)
  • .NET: delta.Data.DeltaContent (PascalCase)

AI generated by SDK Consistency Review Agent for #492

@github-actions
Copy link

Cross-SDK Consistency Review

This PR adds chat samples for all four SDKs, which is great! However, I've identified several critical bugs and consistency issues that need to be addressed:

🐛 Critical Bugs

  1. Python sample will crash - Uses async with CopilotClient() but the SDK doesn't implement __aenter__/__aexit__ methods
  2. Node.js field access bug - Uses event.data.delta instead of event.data.deltaContent
  3. Python field access bug - Uses event.data.delta instead of event.data.delta_content

⚠️ Cross-SDK Inconsistencies

Lifecycle Management Patterns

The samples show different approaches to client/session lifecycle:

SDK Pattern Status
.NET await using (automatic disposal) ✅ Proper RAII
Go Explicit Start()/Stop() + defer ✅ Explicit cleanup
Python Attempted async with ❌ Not implemented
Node.js No cleanup shown ⚠️ Potential resource leak

Recommendation: Each SDK should demonstrate proper cleanup following its language's idioms:

  • Python should use explicit await client.start() / await client.stop() with try/finally
  • Node.js should either handle SIGINT or document that cleanup happens on process exit

Field Naming (Working as Designed)

The event field names correctly follow language conventions:

  • Python: delta_content (snake_case) ✅
  • Node.js: deltaContent (camelCase) ✅
  • Go: DeltaContent (PascalCase) ✅
  • .NET: DeltaContent (PascalCase) ✅

However, the samples are accessing the wrong field names (see inline comments).

📝 Summary

The samples demonstrate equivalent functionality across languages, but the bugs will prevent Python and Node.js samples from running correctly. Once these are fixed, the samples will be excellent references for developers!

AI generated by SDK Consistency Review Agent


session.on((event: SessionEvent) => {
if (event.type === "assistant.message_delta") {
process.stdout.write(event.data?.delta ?? "");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The field name should be deltaContent (camelCase), not delta.

According to nodejs/src/generated/session-events.ts, the field is named deltaContent:

process.stdout.write(event.data?.deltaContent ?? "");

AI generated by SDK Consistency Review Agent for #492

import { CopilotClient, type SessionEvent } from "@github/copilot-sdk";

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK Inconsistency - Missing Cleanup: The Node.js sample doesn't close/cleanup the client, which may leave the CLI subprocess running.

For consistency with other SDKs and proper resource management, consider adding cleanup. While Node.js doesn't have a built-in disposal pattern like .NET's using or Python's context managers, you could:

  1. Handle process exit signals:
process.on('SIGINT', async () => {
    await session.destroy();
    await client.stop();
    process.exit();
});
  1. Or document that the CLI process terminates when the parent exits (if that's the intended behavior).

Note: Go uses explicit defer, .NET uses await using, but Node.js has no cleanup shown.

AI generated by SDK Consistency Review Agent for #492

- Add samples/chat.ts for Node.js/TypeScript
- Add samples/chat.py for Python
- Add samples/chat.go for Go
- Add samples/Chat.cs for .NET
- Update each SDK README with instructions to run the sample

Each sample is a minimal (~30-50 lines) interactive chat loop that:
- Prompts user for input
- Sends to Copilot and waits for idle
- Streams response deltas to console

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations. Great work! The samples maintain excellent consistency while respecting language-specific idioms.

Consistency Summary

All four samples (Node.js, Python, Go, .NET) demonstrate:

Parallel structure and flow:

  • Client and session creation
  • Event subscription for assistant.reasoning and tool.execution_start
  • Interactive prompt loop using sendAndWait/send_and_wait/SendAndWait/SendAndWaitAsync

Consistent API naming (following language conventions):

  • Node.js (camelCase): createSession(), sendAndWait()
  • Python (snake_case): create_session(), send_and_wait()
  • Go (PascalCase for exported): CreateSession(), SendAndWait()
  • .NET (PascalCase with Async suffix): CreateSessionAsync(), SendAndWaitAsync()

Consistent event types: assistant.reasoning and tool.execution_start (with language-specific casing: toolName vs tool_name vs ToolName)

README documentation: All four READMEs get "Run the Sample" sections with consistent formatting

Language-Specific Idioms (Appropriate Differences)

The samples correctly use language-specific patterns:

  • Python: Uses async with CopilotClient() context manager for automatic cleanup (idiomatic Python)
  • Go: Uses defer for cleanup and explicit error handling
  • .NET: Uses await using for async disposal (C# 8.0+)
  • Node.js: Manual client lifecycle (no automatic disposal needed)

No consistency issues found. The PR successfully adds feature-equivalent samples to all four SDK languages! 🎉

AI generated by SDK Consistency Review Agent



async def main():
async with CopilotClient() as client:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency: Python sample uses unsupported async context manager

The Python SDK's CopilotClient does not implement __aenter__/__aexit__ methods, so using async with CopilotClient() as client: will fail at runtime.

Based on the E2E tests and SDK documentation, the correct pattern should be:

client = CopilotClient()
try:
    await client.start()
    session = await client.create_session()
    # ... rest of the code
finally:
    await client.stop()

This matches the pattern used in the .NET, Go, and Node.js samples (accounting for language idioms). Each explicitly manages the client lifecycle with start/stop or using/defer statements.

AI generated by SDK Consistency Review Agent for #492

@github-actions
Copy link

Cross-SDK Consistency Review

Thank you for adding chat samples for all four SDK languages! This is a great addition that helps developers get started quickly. I've reviewed the samples for cross-language consistency and found two issues:

🔴 Critical Issue: Python sample will not run

The Python sample uses async with CopilotClient() as client: (line 10 in python/samples/chat.py), but the Python SDK doesn't implement the async context manager protocol (__aenter__/__aexit__). This will cause an AttributeError at runtime.

Fix needed: Update the Python sample to use explicit await client.start() and await client.stop() like the E2E tests demonstrate, matching the lifecycle management pattern used in the other languages.

🟡 Minor Inconsistency: .NET sample missing tool arguments

The .NET sample only logs [tool: {tool.Data.ToolName}] (line 15 in dotnet/samples/Chat.cs), while Node.js, Python, and Go all include the tool arguments in their output for debugging purposes. Consider adding tool.Data.Arguments to the output for consistency.

✅ Otherwise well done!

The samples are remarkably consistent in:

  • Structure and flow (create client → create session → event subscription → interactive loop)
  • Event handling (both samples listen to assistant.reasoning and tool.execution_start)
  • User experience (same prompts, blue colored debug output, Ctrl+C to exit message)
  • API usage patterns (accounting for language conventions)

Once the Python runtime issue is fixed, these will be excellent getting-started examples!

AI generated by SDK Consistency Review Agent

Console.WriteLine($"[reasoning: {reasoning.Data.Content}]");
break;
case ToolExecutionStartEvent tool:
Console.WriteLine($"[tool: {tool.Data.ToolName}]");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor inconsistency: Missing tool arguments in output

The .NET sample only logs the tool name, while the Node.js, Python, and Go samples all include the tool arguments in their output.

For consistency across all SDK samples, consider adding the arguments:

case ToolExecutionStartEvent tool:
    var args = System.Text.Json.JsonSerializer.Serialize(tool.Data.Arguments);
    Console.WriteLine($"[tool: {tool.Data.ToolName} {args}]");
    break;

This would match the pattern in:

  • Node.js (line 13): `[tool: ${event.data.toolName} ${JSON.stringify(event.data.arguments)}]`
  • Python (line 18): f"[tool: {event.data.tool_name} {json.dumps(event.data.arguments)}]"
  • Go (lines 40-41): fmt.Sprintf("[tool: %s %s]", *event.Data.ToolName, args)

AI generated by SDK Consistency Review Agent for #492

SteveSandersonMS and others added 2 commits February 17, 2026 12:05
event.type is a SessionEventType enum, not a string - use .value for comparison

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

Cross-SDK Consistency Review ✅

I've reviewed this PR which adds minimal interactive chat samples for all four SDK languages (Node.js, Python, Go, .NET).

Overall Assessment

The samples are well-structured and demonstrate equivalent functionality across all SDKs:

  • ✅ Creating a client and session with default options
  • ✅ Subscribing to events (assistant.reasoning and tool.execution_start)
  • ✅ Interactive prompt loop using sendAndWait
  • ✅ Colored console output for event logging
  • ✅ Appropriate language idioms (camelCase for Node/TS, snake_case for Python, PascalCase for public Go/C# APIs)

Issue Found

One consistency issue identified: The Python sample is missing explicit cleanup of the client and session, while Go uses defer client.Stop() + defer session.Destroy() and .NET uses await using for automatic disposal. See inline comment for details.

README Consistency

All four README files have been updated with a "Run the Sample" section showing the appropriate commands for each language. ✅

Great work on maintaining feature parity across all SDKs! 🎉

AI generated by SDK Consistency Review Agent


async def main():
client = CopilotClient()
await client.start()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency issue: The Python sample is missing cleanup of the client and session.

For consistency with Go (which uses defer client.Stop() and defer session.Destroy()) and .NET (which uses await using for automatic disposal), the Python sample should properly clean up resources.

Suggested fix - update main() to use proper cleanup:

async def main():
    client = CopilotClient()
    await client.start()
    try:
        session = await client.create_session()
        # ... event handler and chat loop ...
    finally:
        await client.stop()

This would match the explicit resource management patterns in Go and .NET SDKs.

AI generated by SDK Consistency Review Agent for #492

SteveSandersonMS and others added 4 commits February 17, 2026 12:36
- Add CLIArgs option to ClientOptions for passing extra CLI arguments
- Capture stderr from CLI process for better error messages
- Add processDone channel to signal when CLI exits unexpectedly
- Propagate process exit errors to pending JSON-RPC requests
- Add E2E test verifying error reporting when CLI fails to start

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

Cross-SDK Consistency Review

Thanks for adding chat samples across all four SDKs! The samples demonstrate excellent consistency in structure and functionality. However, I've identified one API inconsistency that this PR introduces.

✅ Chat Samples - Excellent Consistency

The chat samples across all four SDKs follow parallel patterns:

  • All create a client and session with default options
  • All subscribe to events (reasoning, tool execution) with appropriate colored output
  • All implement an interactive prompt loop using sendAndWait/send_and_wait/SendAndWait/SendAndWaitAsync
  • All properly account for language-specific idioms (camelCase vs snake_case vs PascalCase)

❌ API Inconsistency: CLIArgs Feature

This PR adds CLIArgs support to the Go SDK (in go/types.go and go/client.go), which now brings feature parity with Node.js and .NET. However, Python is missing this feature entirely:

SDK Field Name Status
Node.js cliArgs ✅ Already exists
.NET CliArgs ✅ Already exists
Go CLIArgs ✅ Added in this PR
Python cli_args Does not exist

Impact: Users cannot pass custom CLI arguments when creating a CopilotClient in Python, while all other SDKs support this.

Recommendation: Add cli_args: list[str] to Python's CopilotClientOptions in python/copilot/types.py and implement the corresponding logic in python/copilot/client.py to maintain feature parity across all SDKs.


Note: The Go changes also include improved error handling (capturing stderr for better error messages when CLI fails to start). This is a quality improvement and doesn't need to be mirrored in other SDKs immediately, though it could be beneficial to add similar error reporting to the other SDKs in a future PR.

AI generated by SDK Consistency Review Agent

Node.js:
- Add stderrBuffer and processExitPromise for early failure detection
- Race verifyProtocolVersion against process exit
- Add test: should report error with stderr when CLI fails to start

Python:
- Add cli_args option to CopilotClientOptions
- Add ProcessExitedError and stderr capture in jsonrpc
- Add test: test_should_report_error_with_stderr_when_cli_fails_to_start

.NET:
- Add StderrBuffer to capture CLI stderr output
- Handle ConnectionLostException with stderr in error message
- Add test: Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations (Node.js, Python, Go, .NET). Great work maintaining feature parity!

Summary

This PR adds minimal interactive chat samples and improves error handling across all SDKs. The changes demonstrate excellent cross-language consistency.

✅ What's Consistent

1. Chat Samples - All four implementations demonstrate the same functionality:

  • Client and session creation with default options
  • Event subscription for reasoning and tool execution
  • Interactive prompt loop using sendAndWait/SendAndWaitAsync
  • Color-coded output for better UX
  • ~30-50 lines each, keeping them focused and minimal

2. Error Handling - All SDKs now capture and report CLI stderr consistently:

  • Node.js: stderrBuffer + processExitPromise racing mechanism
  • Python: ProcessExitedError + stderr capture in jsonrpc client
  • Go: stderrBuf + processDone channel pattern
  • .NET: StringBuilder stderrBuffer + ConnectionLostException handling

3. CLI Args Support - All SDKs support custom CLI arguments:

  • Node.js: cliArgs?: string[]
  • Python: cli_args: list[str]
  • Go: CLIArgs []string
  • .NET: CliArgs string[]?

All prepend user args before SDK-managed flags consistently.

4. Test Coverage - All SDKs include E2E tests verifying stderr capture with the same approach (passing invalid --nonexistent-flag-for-testing and asserting stderr appears in error message).

🎯 API Naming

The naming conventions properly follow each language's idioms:

  • TypeScript: sendAndWait, createSession (camelCase)
  • Python: send_and_wait, create_session (snake_case)
  • Go: SendAndWait, CreateSession (PascalCase for exported APIs)
  • .NET: SendAndWaitAsync, CreateSessionAsync (PascalCase with async suffix)

📝 Recommendation

No changes needed - this PR maintains excellent cross-SDK consistency. The samples provide a great getting-started experience that's parallel across all languages, and the error handling improvements benefit all users equally.

The only minor observation: README updates are consistent, all pointing users to run their respective samples. Nice touch adding the "Run the Sample" sections!

AI generated by SDK Consistency Review Agent

Go SDK:
- Change processDone from buffered error channel to closed signal channel
- Store processError separately and use mutex for thread-safe access
- Check for process exit before sending requests (fail fast)
- Subsequent requests after process exit now get the same stderr error

All SDKs:
- Enhanced CLI error tests to verify subsequent calls also fail properly
- Tests now call createSession/send instead of just ping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS and others added 4 commits February 17, 2026 13:18
- Node.js: expect 'Connection is closed'
- Python: expect 'Invalid argument'
- .NET: expect 'exited'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assert.Contains("exited", ex2.Message, StringComparison.OrdinalIgnoreCase);

// Cleanup - ForceStop should handle the disconnected state gracefully
try { await client.ForceStopAsync(); } catch (Exception) { /* Expected */ }

def on_event(event):
output = None
if event.type.value == "assistant.reasoning":

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK Consistency Issue: Python uses event.type.value to access the event type, while Node.js, Go, and .NET access event.type directly.

This inconsistency suggests either:

  1. Python's event.type is an enum that requires .value access, or
  2. The API should be normalized to allow direct string comparison like the other SDKs

Consider whether the Python SDK could support direct comparison (e.g., event.type == "assistant.reasoning") for consistency with other languages.

AI generated by SDK Consistency Review Agent for #492

import { CopilotClient, type SessionEvent } from "@github/copilot-sdk";

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK Consistency Issue: The Node.js sample doesn't call any start method on the client, while Python calls await client.start() and Go calls client.Start(ctx) before using the client.

For consistency, if Node.js clients don't require an explicit start call (auto-start on first use), this should be documented. Alternatively, if Node.js supports a start() method, it should be called here to match the pattern in Python and Go samples.

AI generated by SDK Consistency Review Agent for #492

try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nBye!")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK Consistency Issue: The Python sample lacks resource cleanup. According to the memory, Python CopilotClient must be started/stopped explicitly.

For consistency with Go (which uses defer client.Stop()) and .NET (which uses await using), consider adding:

async def main():
    client = CopilotClient()
    try:
        await client.start()
        session = await client.create_session()
        # ... rest of code ...
    finally:
        await client.stop()

This ensures proper cleanup when the user presses Ctrl+C.

AI generated by SDK Consistency Review Agent for #492

}
}

main().catch(console.error);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK Consistency Issue: The Node.js sample lacks resource cleanup, while:

  • Go uses defer client.Stop() and defer session.Destroy()
  • .NET uses await using for automatic disposal
  • Python should also add cleanup but currently doesn't

Consider adding cleanup in a try-finally block or using signal handlers for graceful shutdown to match the cleanup patterns in other SDKs.

AI generated by SDK Consistency Review Agent for #492

@github-actions
Copy link

Cross-SDK Consistency Review

Thank you for adding samples across all four SDKs! This is a great addition for users. However, I've identified several consistency issues that should be addressed:

🔴 Critical: Client Lifecycle Management

The samples have inconsistent client initialization patterns:

  • Python & Go: Explicitly call client.start() / client.Start(ctx) before use
  • Node.js & .NET: No explicit start call

Recommendation: Document whether auto-start is intentional for Node.js/.NET, or align all SDKs to require (or not require) explicit start calls.

🟡 Important: Resource Cleanup

The samples have inconsistent cleanup patterns:

  • Go: ✅ Uses defer client.Stop() and defer session.Destroy()
  • .NET: ✅ Uses await using for automatic disposal
  • Node.js: ❌ No cleanup
  • Python: ❌ No cleanup (should add finally: await client.stop() per the API docs)

Recommendation: Add proper cleanup to Node.js and Python samples. Python's documentation indicates start()/stop() must be called explicitly, so a try-finally block would be appropriate.

🟡 Minor: Event Type Access

  • Python: Uses event.type.value (accessing enum value)
  • Others: Use event.type directly

This may be a language-specific requirement, but consider whether Python's API could support direct comparison for consistency.


Summary: While these are sample/demo files, they serve as the primary learning resource for SDK users. Ensuring consistency in lifecycle management and resource cleanup patterns across languages will help users avoid common pitfalls and understand best practices.

I've added specific inline comments on each file with suggestions for alignment.

AI generated by SDK Consistency Review Agent


func main() {
ctx := context.Background()
cliPath := filepath.Join("..", "..", "nodejs", "node_modules", "@github", "copilot", "index.js")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This hardcoded CLI path is specific to the repo structure and won't work for users running the sample outside the repo. Consider documenting this or using the default CLI discovery mechanism (which would match the other SDK samples that don't specify CLIPath).

AI generated by SDK Consistency Review Agent for #492

- Go: Add sync.WaitGroup to ensure stderr is fully read before accessing buffer
- Python: Accept platform-specific error messages (EINVAL on Windows, EPIPE on Linux)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

✅ Cross-SDK Consistency Review

I've analyzed this PR across all four SDK implementations and found excellent consistency in both features and API design. The PR successfully implements two key improvements uniformly across all SDKs:

1. ✅ CLI Args Support

All SDKs now properly support passing custom CLI arguments:

  • Node.js: cliArgs (already existed, used in samples)
  • Python: cli_args (newly added) ✨
  • Go: CLIArgs (newly added) ✨
  • .NET: CliArgs (already existed)

All implementations correctly prepend user args before SDK-managed args, maintaining consistent behavior.

2. ✅ Enhanced Error Handling for CLI Process Exit

All SDKs now capture and include stderr output when the CLI process exits unexpectedly:

Implementation pattern (consistent across all):

  • Capture stderr in a buffer during CLI process lifetime
  • On process exit, include stderr in error messages
  • Fail pending RPC requests immediately with the stderr context
  • Race RPC calls against process exit to detect early failures

Node.js: Uses stderrBuffer + processExitPromise to race against RPC calls
Python: Adds ProcessExitedError exception with _stderr_output capture in jsonrpc client
Go: Uses stderrBuf + processDone channel to signal all pending requests
.NET: Uses stderrBuffer + ConnectionLostException handling in InvokeRpcAsync

3. ✅ Chat Samples

Each SDK includes a minimal chat sample (~30-50 lines) demonstrating:

  • Client/session creation
  • Event subscription for reasoning and tool execution
  • Interactive prompt loop with sendAndWait / send_and_wait / SendAndWait
  • Proper async patterns for each language

Naming consistency (accounting for language conventions):

  • Node.js: sendAndWait, createSession, event type
  • Python: send_and_wait, create_session, event type.value
  • Go: SendAndWait, CreateSession, event Type
  • .NET: SendAndWaitAsync, CreateSessionAsync, pattern matching on event types

Summary

No consistency issues found. The PR demonstrates excellent adherence to cross-SDK design principles:

  • Feature parity maintained across all four languages
  • API naming follows each language's conventions (camelCase/snake_case/PascalCase)
  • Error handling behavior is uniform
  • Samples are functionally equivalent with appropriate language idioms

Great work! 🎉

AI generated by SDK Consistency Review Agent

SteveSandersonMS and others added 2 commits February 17, 2026 13:32
StderrPipe gets closed by cmd.Wait() before io.Copy can finish reading.
Using cmd.Stderr = &buffer ensures all stderr is captured before Wait returns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use StderrPipe with explicit ReadAll in goroutine, wait for EOF signal
before accessing buffer. This ensures all stderr data is captured even
when the process exits quickly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

Cross-SDK Consistency Review ✅

I've reviewed this PR for consistency across all four SDK implementations (Node.js, Python, Go, .NET). Overall, excellent work maintaining feature parity! 🎉

✅ Consistent Implementations

1. CLI Args Support
All SDKs now support passing custom CLI arguments:

  • Node.js: cliArgs: string[]
  • Python: cli_args: list[str]
  • Go: CLIArgs []string
  • .NET: CliArgs string[]?

The implementation is consistent - user args are prepended before SDK-managed args in all languages.

2. Error Handling for CLI Termination
All SDKs now properly handle unexpected CLI exits with stderr capture:

  • All capture stderr output in a buffer
  • All include stderr in error messages when CLI exits unexpectedly
  • All provide clear error messages distinguishing between process exit and communication errors
  • Implementations use language-appropriate patterns (Promise rejection in Node.js, channels in Go, async exceptions in Python, ConnectionLostException handling in .NET)

3. Chat Samples
All four samples are beautifully consistent in structure and functionality:

  • Create client and session with default options
  • Subscribe to events (reasoning, tool execution) with colored output
  • Interactive prompt loop using sendAndWait/send_and_wait/SendAndWaitAsync
  • ~30-50 lines each
  • Similar user experience across languages

4. Tests
All SDKs include E2E tests verifying that passing --nonexistent-flag-for-testing produces a clear error with stderr output.

📝 Minor Issue

I've left one inline comment about adding XML documentation to the .NET CliArgs property to match the documentation consistency of other SDKs.


Summary: This PR successfully maintains cross-SDK consistency while adding valuable features (CLI args support, better error handling) and examples (chat samples) to all four languages. The only minor issue is a missing XML doc comment in .NET.

AI generated by SDK Consistency Review Agent

@SteveSandersonMS SteveSandersonMS merged commit ed51ab8 into main Feb 17, 2026
28 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesa/add-samples branch February 17, 2026 13:46
@github-actions
Copy link

✅ Cross-SDK Consistency Review

I've completed a comprehensive review of this PR for consistency across all four SDK implementations (Node.js, Python, Go, and .NET). This PR demonstrates excellent cross-language consistency!

Features Verified

1. CLI Args Support ✅

All SDKs now support passing extra CLI arguments with proper language-specific naming:

  • Node.js: cliArgs?: string[] (camelCase)
  • Python: cli_args: list[str] (snake_case)
  • Go: CLIArgs []string (PascalCase)
  • .NET: CliArgs string[]? (PascalCase)

2. Error Handling for CLI Process Exit ✅

All SDKs implement consistent error handling when the CLI terminates unexpectedly:

  • Node.js: processExitPromise + stderrBuffer
  • Python: Process exit detection with stderr capture
  • Go: processDone channel + processError
  • .NET: stderrBuffer (StringBuilder)

All implementations:

  • Capture stderr output from the CLI
  • Include stderr in error messages
  • Fail cleanly on subsequent RPC calls (no hanging)
  • Handle startup errors (e.g., invalid CLI args) with clear error messages

3. Test Coverage ✅

All SDKs include parallel test cases:

  • Test with --nonexistent-flag-for-testing to trigger CLI startup failure
  • Verify stderr appears in the error message
  • Verify subsequent operations fail cleanly instead of hanging

4. Chat Samples ✅

All four languages have consistent interactive chat samples (~30-50 lines):

  • Create client and session with default options
  • Subscribe to events (reasoning, tool execution)
  • Interactive prompt loop using sendAndWait/send_and_wait/SendAndWait
  • Blue color output for tool/reasoning events

5. Documentation ✅

All READMEs updated with "Run the Sample" sections providing clear build/run instructions.

Summary

No consistency issues found. This PR adds two important features (CLI args support and improved error handling) plus helpful samples, and does so with excellent feature parity across all four SDK languages. Great work maintaining consistency! 🎉

AI generated by SDK Consistency Review Agent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant