Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sdk-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
COPILOT_CLI_PATH: ${{ steps.cli-path.outputs.path }}
run: ./test.sh
run: /bin/bash test.sh

python-sdk:
name: "Python SDK Tests"
Expand Down
44 changes: 44 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,50 @@ public async Task<PingResponse> PingAsync(string? message = null, CancellationTo
"ping", [new { message }], cancellationToken);
}

/// <summary>
/// Gets CLI status including version and protocol information.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves with the status response containing version and protocol version.</returns>
/// <exception cref="InvalidOperationException">Thrown when the client is not connected.</exception>
public async Task<GetStatusResponse> GetStatusAsync(CancellationToken cancellationToken = default)
{
var connection = await EnsureConnectedAsync(cancellationToken);

return await connection.Rpc.InvokeWithCancellationAsync<GetStatusResponse>(
"status.get", [], cancellationToken);
}

/// <summary>
/// Gets current authentication status.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves with the authentication status.</returns>
/// <exception cref="InvalidOperationException">Thrown when the client is not connected.</exception>
public async Task<GetAuthStatusResponse> GetAuthStatusAsync(CancellationToken cancellationToken = default)
{
var connection = await EnsureConnectedAsync(cancellationToken);

return await connection.Rpc.InvokeWithCancellationAsync<GetAuthStatusResponse>(
"auth.getStatus", [], cancellationToken);
}

/// <summary>
/// Lists available models with their metadata.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves with a list of available models.</returns>
/// <exception cref="InvalidOperationException">Thrown when the client is not connected or not authenticated.</exception>
public async Task<List<ModelInfo>> ListModelsAsync(CancellationToken cancellationToken = default)
{
var connection = await EnsureConnectedAsync(cancellationToken);

var response = await connection.Rpc.InvokeWithCancellationAsync<GetModelsResponse>(
"models.list", [], cancellationToken);

return response.Models;
}

/// <summary>
/// Gets the ID of the most recently used session.
/// </summary>
Expand Down
24 changes: 23 additions & 1 deletion dotnet/src/Generated/SessionEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//
// Generated from: @github/copilot/session-events.schema.json
// Generated by: scripts/generate-session-types.ts
// Generated at: 2026-01-21T14:50:29.306Z
// Generated at: 2026-01-22T04:11:05.393Z
//
// To update these types:
// 1. Update the schema in copilot-agent-runtime
Expand Down Expand Up @@ -58,6 +58,7 @@ namespace GitHub.Copilot.SDK
[JsonDerivedType(typeof(SystemMessageEvent), "system.message")]
[JsonDerivedType(typeof(ToolExecutionCompleteEvent), "tool.execution_complete")]
[JsonDerivedType(typeof(ToolExecutionPartialResultEvent), "tool.execution_partial_result")]
[JsonDerivedType(typeof(ToolExecutionProgressEvent), "tool.execution_progress")]
[JsonDerivedType(typeof(ToolExecutionStartEvent), "tool.execution_start")]
[JsonDerivedType(typeof(ToolUserRequestedEvent), "tool.user_requested")]
[JsonDerivedType(typeof(UserMessageEvent), "user.message")]
Expand Down Expand Up @@ -389,6 +390,18 @@ public partial class ToolExecutionPartialResultEvent : SessionEvent
public required ToolExecutionPartialResultData Data { get; set; }
}

/// <summary>
/// Event: tool.execution_progress
/// </summary>
public partial class ToolExecutionProgressEvent : SessionEvent
{
[JsonIgnore]
public override string Type => "tool.execution_progress";

[JsonPropertyName("data")]
public required ToolExecutionProgressData Data { get; set; }
}

/// <summary>
/// Event: tool.execution_complete
/// </summary>
Expand Down Expand Up @@ -850,6 +863,15 @@ public partial class ToolExecutionPartialResultData
public required string PartialOutput { get; set; }
}

public partial class ToolExecutionProgressData
{
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }

[JsonPropertyName("progressMessage")]
public required string ProgressMessage { get; set; }
}

public partial class ToolExecutionCompleteData
{
[JsonPropertyName("toolCallId")]
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/SdkProtocolVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static class SdkProtocolVersion
/// <summary>
/// The SDK protocol version.
/// </summary>
public const int Version = 1;
public const int Version = 2;

/// <summary>
/// Gets the SDK protocol version.
Expand Down
147 changes: 147 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,150 @@ public class PingResponse
public long Timestamp { get; set; }
public int? ProtocolVersion { get; set; }
}

/// <summary>
/// Response from status.get
/// </summary>
public class GetStatusResponse
{
/// <summary>Package version (e.g., "1.0.0")</summary>
[JsonPropertyName("version")]
public string Version { get; set; } = string.Empty;

/// <summary>Protocol version for SDK compatibility</summary>
[JsonPropertyName("protocolVersion")]
public int ProtocolVersion { get; set; }
}

/// <summary>
/// Response from auth.getStatus
/// </summary>
public class GetAuthStatusResponse
{
/// <summary>Whether the user is authenticated</summary>
[JsonPropertyName("isAuthenticated")]
public bool IsAuthenticated { get; set; }

/// <summary>Authentication type (user, env, gh-cli, hmac, api-key, token)</summary>
[JsonPropertyName("authType")]
public string? AuthType { get; set; }

/// <summary>GitHub host URL</summary>
[JsonPropertyName("host")]
public string? Host { get; set; }

/// <summary>User login name</summary>
[JsonPropertyName("login")]
public string? Login { get; set; }

/// <summary>Human-readable status message</summary>
[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; set; }
}

/// <summary>
/// Model vision-specific limits
/// </summary>
public class ModelVisionLimits
{
[JsonPropertyName("supported_media_types")]
public List<string> SupportedMediaTypes { get; set; } = new();

[JsonPropertyName("max_prompt_images")]
public int MaxPromptImages { get; set; }

[JsonPropertyName("max_prompt_image_size")]
public int MaxPromptImageSize { get; set; }
}

/// <summary>
/// Model limits
/// </summary>
public class ModelLimits
{
[JsonPropertyName("max_prompt_tokens")]
public int? MaxPromptTokens { get; set; }

[JsonPropertyName("max_context_window_tokens")]
public int MaxContextWindowTokens { get; set; }

[JsonPropertyName("vision")]
public ModelVisionLimits? Vision { get; set; }
}

/// <summary>
/// Model support flags
/// </summary>
public class ModelSupports
{
[JsonPropertyName("vision")]
public bool Vision { get; set; }
}

/// <summary>
/// Model capabilities and limits
/// </summary>
public class ModelCapabilities
{
[JsonPropertyName("supports")]
public ModelSupports Supports { get; set; } = new();

[JsonPropertyName("limits")]
public ModelLimits Limits { get; set; } = new();
}

/// <summary>
/// Model policy state
/// </summary>
public class ModelPolicy
{
[JsonPropertyName("state")]
public string State { get; set; } = string.Empty;

[JsonPropertyName("terms")]
public string Terms { get; set; } = string.Empty;
}

/// <summary>
/// Model billing information
/// </summary>
public class ModelBilling
{
[JsonPropertyName("multiplier")]
public double Multiplier { get; set; }
}

/// <summary>
/// Information about an available model
/// </summary>
public class ModelInfo
{
/// <summary>Model identifier (e.g., "claude-sonnet-4.5")</summary>
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;

/// <summary>Display name</summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;

/// <summary>Model capabilities and limits</summary>
[JsonPropertyName("capabilities")]
public ModelCapabilities Capabilities { get; set; } = new();

/// <summary>Policy state</summary>
[JsonPropertyName("policy")]
public ModelPolicy? Policy { get; set; }

/// <summary>Billing information</summary>
[JsonPropertyName("billing")]
public ModelBilling? Billing { get; set; }
}

/// <summary>
/// Response from models.list
/// </summary>
public class GetModelsResponse
{
[JsonPropertyName("models")]
public List<ModelInfo> Models { get; set; } = new();
}
83 changes: 83 additions & 0 deletions dotnet/test/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,87 @@ public async Task Should_Force_Stop_Without_Cleanup()

Assert.Equal(ConnectionState.Disconnected, client.State);
}

[Fact]
public async Task Should_Get_Status_With_Version_And_Protocol_Info()
{
using var client = new CopilotClient(new CopilotClientOptions { CliPath = _cliPath, UseStdio = true });

try
{
await client.StartAsync();

var status = await client.GetStatusAsync();
Assert.NotNull(status.Version);
Assert.NotEmpty(status.Version);
Assert.True(status.ProtocolVersion >= 1);

await client.StopAsync();
}
finally
{
await client.ForceStopAsync();
}
}

[Fact]
public async Task Should_Get_Auth_Status()
{
using var client = new CopilotClient(new CopilotClientOptions { CliPath = _cliPath, UseStdio = true });

try
{
await client.StartAsync();

var authStatus = await client.GetAuthStatusAsync();
// isAuthenticated is a bool, just verify we got a response
if (authStatus.IsAuthenticated)
{
Assert.NotNull(authStatus.AuthType);
Assert.NotNull(authStatus.StatusMessage);
}

await client.StopAsync();
}
finally
{
await client.ForceStopAsync();
}
}

[Fact]
public async Task Should_List_Models_When_Authenticated()
{
using var client = new CopilotClient(new CopilotClientOptions { CliPath = _cliPath, UseStdio = true });

try
{
await client.StartAsync();

var authStatus = await client.GetAuthStatusAsync();
if (!authStatus.IsAuthenticated)
{
// Skip if not authenticated - models.list requires auth
await client.StopAsync();
return;
}

var models = await client.ListModelsAsync();
Assert.NotNull(models);
if (models.Count > 0)
{
var model = models[0];
Assert.NotNull(model.Id);
Assert.NotEmpty(model.Id);
Assert.NotNull(model.Name);
Assert.NotNull(model.Capabilities);
}

await client.StopAsync();
}
finally
{
await client.ForceStopAsync();
}
}
}
Loading
Loading