diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs index 69514edda..0fb22304d 100644 --- a/dotnet/src/Canvas.cs +++ b/dotnet/src/Canvas.cs @@ -2,9 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -55,19 +57,19 @@ public sealed class ExtensionInfo public string Name { get; set; } = string.Empty; } -/// Structured error returned from canvas handlers. +/// Structured exception returned from canvas handlers. /// /// Throw this from implementations to surface a /// machine-readable error code to the runtime. Any other exception is wrapped /// in a generic canvas_handler_error envelope. /// [Experimental(Diagnostics.Experimental)] -public sealed class CanvasError : Exception +public sealed class CanvasException : Exception { - /// Initializes a new . + /// Initializes a new . /// Machine-readable error code. /// Human-readable message. - public CanvasError(string code, string message) : base(message) + public CanvasException(string code, string message) : base(message) { Code = code; } @@ -76,15 +78,15 @@ public CanvasError(string code, string message) : base(message) public string Code { get; } /// - /// Default error returned when a custom action has no handler. + /// Default exception returned when a custom action has no handler. /// - public static CanvasError NoHandler() => new( + public static CanvasException NoHandler() => new( "canvas_action_no_handler", "No handler implemented for this canvas action"); } /// -/// Internal helpers used by the session runtime to translate +/// Internal helpers used by the session runtime to translate /// (and other handler-thrown exceptions) into structured JSON-RPC error responses. /// internal static class CanvasErrorHelpers @@ -99,31 +101,17 @@ public static LocalRpcInvocationException HandlerError(string message) => Build( "canvas_handler_error", message); - public static LocalRpcInvocationException ToRpcException(CanvasError error) => Build(error.Code, error.Message); + public static LocalRpcInvocationException ToRpcException(CanvasException error) => Build(error.Code, error.Message); private static LocalRpcInvocationException Build(string code, string message) { - var json = JsonSerializer.Serialize( - new CanvasErrorPayload { Code = code, Message = message }, - CanvasJsonContext.Default.CanvasErrorPayload); - using var doc = JsonDocument.Parse(json); - return new LocalRpcInvocationException(InternalError, message, doc.RootElement.Clone()); - } - - internal sealed class CanvasErrorPayload - { - [JsonPropertyName("code")] - public string Code { get; set; } = string.Empty; - - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + JsonElement payload = JsonSerializer.SerializeToElement( + new JsonObject { ["code"] = code, ["message"] = message }, + TypesJsonContext.Default.JsonObject); + return new LocalRpcInvocationException(InternalError, message, payload); } } -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(CanvasErrorHelpers.CanvasErrorPayload))] -internal partial class CanvasJsonContext : JsonSerializerContext; - /// /// Provider-side canvas lifecycle handler. /// @@ -155,7 +143,7 @@ public interface ICanvasHandler /// /// Handle a non-lifecycle action declared by the canvas. - /// Default: throws . + /// Default: throws . /// Task OnActionAsync(CanvasProviderInvokeActionRequest context, CancellationToken cancellationToken); } @@ -180,5 +168,5 @@ public virtual Task OnCloseAsync(CanvasProviderCloseRequest context, Cancellatio /// public virtual Task OnActionAsync(CanvasProviderInvokeActionRequest context, CancellationToken cancellationToken) - => Task.FromException(CanvasError.NoHandler()); + => Task.FromException(CanvasException.NoHandler()); } diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index df7170373..1346519ea 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -486,6 +486,13 @@ private async Task HandleIncomingMethodAsync(string methodName, JsonElement mess } catch (Exception ex) when (ex is not OperationCanceledException) { + // `InvokeHandlerAsync` dispatches handlers via reflection + // (`Delegate.DynamicInvoke` / `MethodInfo.Invoke`), which wraps + // any exception thrown inside the user-supplied handler in a + // `TargetInvocationException`. Unwrap so we surface the original + // failure (e.g. `LocalRpcInvocationException`, `CanvasException`) + // to the JSON-RPC error response instead of the reflection + // wrapper. var actual = ex is TargetInvocationException tie && tie.InnerException != null ? tie.InnerException : ex; if (_logger.IsEnabled(LogLevel.Debug)) { diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 08044d685..b23684ada 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -892,15 +892,12 @@ internal void SetCanvasHandler(ICanvasHandler? handler) ClientSessionApis.Canvas = handler is null ? null : new CanvasHandlerAdapter(handler); } + private static readonly JsonElement NullJsonElement = JsonDocument.Parse("null").RootElement.Clone(); + private static JsonElement SerializeActionResult(object? value) { var element = CopilotClient.ToJsonElementForWire(value); - if (element.HasValue) - { - return element.Value; - } - using var doc = JsonDocument.Parse("null"); - return doc.RootElement.Clone(); + return element ?? NullJsonElement; } #pragma warning restore GHCP001 @@ -912,7 +909,7 @@ public async Task OpenAsync(CanvasProviderOpenRequest { return await handler.OnOpenAsync(request, cancellationToken).ConfigureAwait(false); } - catch (CanvasError ce) + catch (CanvasException ce) { throw CanvasErrorHelpers.ToRpcException(ce); } @@ -928,7 +925,7 @@ public async Task CloseAsync(CanvasProviderCloseRequest request, CancellationTok { await handler.OnCloseAsync(request, cancellationToken).ConfigureAwait(false); } - catch (CanvasError ce) + catch (CanvasException ce) { throw CanvasErrorHelpers.ToRpcException(ce); } @@ -945,7 +942,7 @@ public async Task InvokeActionAsync(CanvasProviderInvokeActionRequest re var result = await handler.OnActionAsync(request, cancellationToken).ConfigureAwait(false); return SerializeActionResult(result); } - catch (CanvasError ce) + catch (CanvasException ce) { throw CanvasErrorHelpers.ToRpcException(ce); } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 94130b604..5e06dba60 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace GitHub.Copilot; @@ -3085,6 +3086,7 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(ToolResultObject))] [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(JsonElement?))] +[JsonSerializable(typeof(JsonObject))] [JsonSerializable(typeof(object))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(string[]))] diff --git a/dotnet/test/Unit/CanvasTests.cs b/dotnet/test/Unit/CanvasTests.cs index 6a2e71a58..72b995731 100644 --- a/dotnet/test/Unit/CanvasTests.cs +++ b/dotnet/test/Unit/CanvasTests.cs @@ -86,18 +86,18 @@ public async Task CanvasHandlerBase_DefaultOnClose_Completes() } [Fact] - public async Task CanvasHandlerBase_DefaultOnAction_ThrowsNoHandlerCanvasError() + public async Task CanvasHandlerBase_DefaultOnAction_ThrowsNoHandlerCanvasException() { var handler = new TestHandler(); - var ex = await Assert.ThrowsAsync( + var ex = await Assert.ThrowsAsync( () => handler.OnActionAsync(new CanvasProviderInvokeActionRequest(), CancellationToken.None)); Assert.Equal("canvas_action_no_handler", ex.Code); } [Fact] - public void CanvasError_NoHandler_HasExpectedCode() + public void CanvasException_NoHandler_HasExpectedCode() { - var err = CanvasError.NoHandler(); + var err = CanvasException.NoHandler(); Assert.Equal("canvas_action_no_handler", err.Code); Assert.False(string.IsNullOrEmpty(err.Message)); } diff --git a/go/canvas.go b/go/canvas.go index 49216c3d9..362f2a842 100644 --- a/go/canvas.go +++ b/go/canvas.go @@ -14,6 +14,9 @@ import ( // CanvasDeclaration is the declarative metadata for a single canvas, sent over // the wire on `session.create` / `session.resume`. +// +// Experimental: CanvasDeclaration is part of an experimental wire-protocol +// surface and may change or be removed in future SDK or CLI releases. type CanvasDeclaration struct { // ID is the canvas identifier, unique within the declaring connection. ID string `json:"id"` @@ -29,6 +32,9 @@ type CanvasDeclaration struct { // ExtensionInfo carries stable extension identity for session participants // that provide canvases. +// +// Experimental: ExtensionInfo is part of an experimental wire-protocol +// surface and may change or be removed in future SDK or CLI releases. type ExtensionInfo struct { // Source is the extension namespace/source, e.g. "github-app". Source string `json:"source"` @@ -41,6 +47,9 @@ type ExtensionInfo struct { // Wire envelope: // // { "code": "", "message": "" } +// +// Experimental: CanvasError is part of an experimental wire-protocol +// surface and may change or be removed in future SDK or CLI releases. type CanvasError struct { // Code is the machine-readable error code. Code string `json:"code"` @@ -79,6 +88,9 @@ func CanvasErrorNoHandler() *CanvasError { // // Embed CanvasHandlerDefaults to inherit no-op defaults for OnClose and a // "no handler" error for OnAction. +// +// Experimental: CanvasHandler is part of an experimental wire-protocol +// surface and may change or be removed in future SDK or CLI releases. type CanvasHandler interface { OnOpen(ctx context.Context, c rpc.CanvasProviderOpenRequest) (rpc.CanvasProviderOpenResult, error) OnClose(ctx context.Context, c rpc.CanvasProviderCloseRequest) error @@ -94,6 +106,9 @@ type CanvasHandler interface { // copilot.CanvasHandlerDefaults // } // func (h *myHandler) OnOpen(ctx context.Context, c rpc.CanvasProviderOpenRequest) (rpc.CanvasProviderOpenResult, error) { ... } +// +// Experimental: CanvasHandlerDefaults is part of an experimental wire-protocol +// surface and may change or be removed in future SDK or CLI releases. type CanvasHandlerDefaults struct{} // OnClose returns nil by default. diff --git a/nodejs/src/canvas.ts b/nodejs/src/canvas.ts index 4a3b25206..bcc59c69c 100644 --- a/nodejs/src/canvas.ts +++ b/nodejs/src/canvas.ts @@ -10,7 +10,11 @@ import type { CanvasProviderOpenResult, } from "./generated/rpc.js"; -export type { CanvasJsonSchema, CanvasHostContext } from "./generated/rpc.js"; +export type { + CanvasJsonSchema, + CanvasHostContext, + CanvasHostContextCapabilities, +} from "./generated/rpc.js"; /** * Extension-owned canvases declared via @@ -21,6 +25,9 @@ export type { CanvasJsonSchema, CanvasHostContext } from "./generated/rpc.js"; * pipeline. The SDK routes those requests by `canvasId` to the in-process * handlers bound by `createCanvas`. Re-opening with an existing `instanceId` * is how the host focuses an existing panel; reload is a renderer-only concern. + * + * @experimental Canvas types are part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. */ /** @@ -31,6 +38,9 @@ export type { CanvasJsonSchema, CanvasHostContext } from "./generated/rpc.js"; * * Names MUST NOT start with `canvas.` — that prefix is reserved for * lifecycle verbs. + * + * @experimental This type is part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. */ export interface CanvasAction { /** Action identifier, unique within the canvas. */ @@ -46,6 +56,9 @@ export interface CanvasAction { /** * Declarative metadata for a single canvas, serialized over the wire on * `session.create` / `session.resume`. + * + * @experimental This type is part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. */ export interface CanvasDeclaration { /** Canvas id, unique within the declaring connection. */ @@ -60,7 +73,12 @@ export interface CanvasDeclaration { actions?: Omit[]; } -/** Structured error returned from canvas handlers. */ +/** + * Structured error returned from canvas handlers. + * + * @experimental This class is part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. + */ export class CanvasError extends Error { constructor( public readonly code: string, @@ -82,6 +100,9 @@ export class CanvasError extends Error { /** * Options accepted by {@link createCanvas}. Combines the declarative * {@link CanvasDeclaration} fields with the in-process handler closures. + * + * @experimental This interface is part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. */ export interface CanvasOptions { /** @see CanvasDeclaration.id */ @@ -119,6 +140,9 @@ export interface CanvasOptions { * ergonomics) where other SDKs (Rust, Python, Go, .NET) expose a single * `CanvasHandler` per session that switches on `canvasId`. Both shapes target * the same JSON-RPC wire protocol; the divergence is API ergonomics only. + * + * @experimental This class is part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. */ export class Canvas { readonly declaration: CanvasDeclaration; @@ -156,6 +180,9 @@ export class Canvas { * `DefineTool`'s co-location ergonomics) where other SDKs (Rust, Python, Go, * .NET) expose a single `CanvasHandler` per session that switches on * `canvasId`. Both shapes target the same JSON-RPC wire protocol. + * + * @experimental This function is part of an experimental wire-protocol surface + * and may change or be removed in future SDK or CLI releases. */ export function createCanvas(options: CanvasOptions): Canvas { return new Canvas(options); diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index c39621c0b..c0390a5f2 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -18,6 +18,7 @@ export { type CanvasAction, type CanvasDeclaration, type CanvasHostContext, + type CanvasHostContextCapabilities, type CanvasJsonSchema, type CanvasOptions, } from "./canvas.js"; diff --git a/python/copilot/canvas.py b/python/copilot/canvas.py index 772c15b39..ddbc8539a 100644 --- a/python/copilot/canvas.py +++ b/python/copilot/canvas.py @@ -6,6 +6,11 @@ user-supplied :class:`CanvasHandler`; multiplexing across multiple declared canvases is the implementor's responsibility (for example by switching on ``ctx.canvas_id``). + +.. note:: + + **Experimental.** Canvas types are part of an experimental wire-protocol + surface and may change or be removed in future SDK or CLI releases. """ from __future__ import annotations @@ -44,6 +49,11 @@ class ExtensionInfo: """Stable extension identity for session participants that provide canvases. Serializes to ``{"source": ..., "name": ...}`` on the wire. + + .. note:: + + **Experimental.** This type is part of an experimental wire-protocol + surface and may change or be removed in future SDK or CLI releases. """ source: str @@ -58,7 +68,13 @@ def to_dict(self) -> dict[str, Any]: @dataclass class CanvasDeclaration: - """Declarative metadata for a single canvas, sent on create/resume.""" + """Declarative metadata for a single canvas, sent on create/resume. + + .. note:: + + **Experimental.** This type is part of an experimental wire-protocol + surface and may change or be removed in future SDK or CLI releases. + """ id: str """Canvas identifier, unique within the declaring connection.""" @@ -89,7 +105,13 @@ def to_dict(self) -> dict[str, Any]: class CanvasError(Exception): - """Structured error returned from canvas handlers.""" + """Structured error returned from canvas handlers. + + .. note:: + + **Experimental.** This type is part of an experimental wire-protocol + surface and may change or be removed in future SDK or CLI releases. + """ def __init__(self, code: str, message: str) -> None: self.code = code @@ -118,7 +140,13 @@ def handler_unset(cls) -> CanvasError: class CanvasHandler(ABC): - """Provider-side canvas lifecycle handler.""" + """Provider-side canvas lifecycle handler. + + .. note:: + + **Experimental.** This type is part of an experimental wire-protocol + surface and may change or be removed in future SDK or CLI releases. + """ @abstractmethod async def on_open(self, ctx: CanvasProviderOpenRequest) -> CanvasProviderOpenResult: diff --git a/rust/src/canvas.rs b/rust/src/canvas.rs index 15cbb031d..c87395874 100644 --- a/rust/src/canvas.rs +++ b/rust/src/canvas.rs @@ -1,4 +1,11 @@ //! Canvas declarations, provider callbacks, and host-side canvas RPC types. +//! +//!
+//! +//! **Experimental.** Canvas types are part of an experimental wire-protocol surface +//! and may change or be removed in future SDK or CLI releases. +//! +//!
use async_trait::async_trait; use serde::{Deserialize, Serialize}; @@ -8,11 +15,25 @@ use thiserror::Error; use crate::generated::api_types::CanvasAction; /// JSON Schema object used for canvas inputs and canvas-scoped tools. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
pub type CanvasJsonSchema = serde_json::Map; /// Declarative metadata for a single canvas, sent over the wire on /// `session.create` / `session.resume`. -#[derive(Debug, Clone, Serialize, Deserialize)] +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct CanvasDeclaration { @@ -54,6 +75,13 @@ impl CanvasDeclaration { } /// Structured error returned from canvas handlers. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
#[derive(Debug, Clone, Error, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[error("{code}: {message}")] @@ -83,10 +111,24 @@ impl CanvasError { } /// Result alias for canvas handler methods. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
pub type CanvasResult = Result; /// Provider-side canvas lifecycle handler. /// +///
+/// +/// **Experimental.** This trait is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+/// /// A session installs a single [`CanvasHandler`] (via /// [`SessionConfig::with_canvas_handler`](crate::types::SessionConfig::with_canvas_handler)). /// The handler receives every inbound `canvas.open` / `canvas.close` /