@@ -13522,6 +13841,13 @@ pub enum SessionContextHostType {
}
/// Error classification
+///
+///
+///
+/// **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, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionFsErrorCode {
/// The requested path does not exist.
@@ -13535,6 +13861,13 @@ pub enum SessionFsErrorCode {
}
/// Entry type
+///
+///
+///
+/// **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, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionFsReaddirWithTypesEntryType {
/// The entry is a file.
@@ -13565,6 +13898,13 @@ pub enum SessionFsSetProviderConventions {
}
/// How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected)
+///
+///
+///
+/// **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, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionFsSqliteQueryType {
/// Execute DDL or multi-statement SQL without returning rows.
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs
index 644e49e86..c0c793fe0 100644
--- a/rust/src/generated/rpc.rs
+++ b/rust/src/generated/rpc.rs
@@ -902,7 +902,7 @@ impl<'a> ClientRpcSessions<'a> {
///
/// # Returns
///
- /// The same metadata records, with summary and context fields backfilled where available.
+ /// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted.
///
///
///
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 787697e2e..cad6ee629 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -5,6 +5,7 @@
/// Canvas declarations, provider callbacks, and host-side canvas RPC types.
pub mod canvas;
+mod canvas_dispatch;
/// Bundled CLI binary extraction and caching.
pub(crate) mod embeddedcli;
/// Event handler traits for session lifecycle.
diff --git a/rust/src/session.rs b/rust/src/session.rs
index f216b866b..57181459c 100644
--- a/rust/src/session.rs
+++ b/rust/src/session.rs
@@ -4,14 +4,13 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex as ParkingLotMutex;
-use serde::de::DeserializeOwned;
use serde_json::Value;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::{Instrument, warn};
-use crate::canvas::{CanvasHandler, CanvasInvokeParams, CanvasProviderRequestParams};
+use crate::canvas::CanvasHandler;
use crate::generated::api_types::{LogRequest, ModelSwitchToRequest, OpenCanvasInstance};
use crate::generated::session_events::{
CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, SessionErrorData,
@@ -1735,39 +1734,12 @@ async fn handle_request(
return;
}
- match request.method.as_str() {
- "canvas.open" => {
- let Some(params) =
- parse_request_params::
(client, request.id, &request)
- .await
- else {
- return;
- };
- let result = dispatch_canvas_open(canvas_handler, params).await;
- send_canvas_dispatch_response(client, request.id, result).await;
- }
-
- "canvas.close" => {
- let Some(params) =
- parse_request_params::(client, request.id, &request)
- .await
- else {
- return;
- };
- let result = dispatch_canvas_close(canvas_handler, params).await;
- send_canvas_dispatch_response(client, request.id, result).await;
- }
-
- "canvas.action.invoke" => {
- let Some(params) =
- parse_request_params::(client, request.id, &request).await
- else {
- return;
- };
- let result = dispatch_canvas_action(canvas_handler, params).await;
- send_canvas_dispatch_response(client, request.id, result).await;
- }
+ if request.method.starts_with("canvas.") {
+ crate::canvas_dispatch::dispatch(client, canvas_handler, request).await;
+ return;
+ }
+ match request.method.as_str() {
"hooks.invoke" => {
let params = request.params.as_ref();
let hook_type = params
@@ -2000,112 +1972,6 @@ async fn handle_request(
}
}
-async fn parse_request_params(
- client: &Client,
- id: u64,
- request: &crate::JsonRpcRequest,
-) -> Option
-where
- T: DeserializeOwned,
-{
- let params = request
- .params
- .as_ref()
- .cloned()
- .unwrap_or(Value::Object(serde_json::Map::new()));
- match serde_json::from_value(params) {
- Ok(params) => Some(params),
- Err(error) => {
- let _ = send_error_response(
- client,
- id,
- error_codes::INVALID_PARAMS,
- &format!("invalid params: {error}"),
- )
- .await;
- None
- }
- }
-}
-
-async fn send_canvas_dispatch_response(
- client: &Client,
- id: u64,
- result: crate::canvas::CanvasResult,
-) {
- let response = match result {
- Ok(value) => JsonRpcResponse {
- jsonrpc: "2.0".to_string(),
- id,
- result: Some(value),
- error: None,
- },
- Err(error) => JsonRpcResponse {
- jsonrpc: "2.0".to_string(),
- id,
- result: None,
- error: Some(crate::JsonRpcError {
- code: error_codes::INTERNAL_ERROR,
- message: error.message.clone(),
- data: Some(serde_json::json!({
- "code": error.code,
- "message": error.message,
- })),
- }),
- },
- };
- if let Err(error) = client.send_response(&response).await {
- warn!(
- request_id = id,
- error = %error,
- "failed to send canvas provider response"
- );
- }
-}
-
-fn canvas_handler_or_err(
- handler: Option<&Arc>,
-) -> crate::canvas::CanvasResult<&Arc> {
- handler.ok_or_else(|| {
- crate::canvas::CanvasError::new(
- "canvas_handler_unset",
- "No CanvasHandler installed on this session; \
- call SessionConfig::with_canvas_handler before creating the session.",
- )
- })
-}
-
-async fn dispatch_canvas_open(
- handler: Option<&Arc>,
- params: CanvasProviderRequestParams,
-) -> crate::canvas::CanvasResult {
- let handler = canvas_handler_or_err(handler)?;
- let response = handler.on_open(params.into_open_context()).await?;
- serde_json::to_value(response).map_err(|error| {
- crate::canvas::CanvasError::new(
- "canvas_open_response_serialization_failed",
- format!("failed to serialize canvas.open response: {error}"),
- )
- })
-}
-
-async fn dispatch_canvas_close(
- handler: Option<&Arc>,
- params: CanvasProviderRequestParams,
-) -> crate::canvas::CanvasResult {
- let handler = canvas_handler_or_err(handler)?;
- handler.on_close(params.into_lifecycle_context()).await?;
- Ok(Value::Null)
-}
-
-async fn dispatch_canvas_action(
- handler: Option<&Arc>,
- params: CanvasInvokeParams,
-) -> crate::canvas::CanvasResult {
- let handler = canvas_handler_or_err(handler)?;
- handler.on_action(params.into_action_context()).await
-}
-
async fn send_error_response(
client: &Client,
id: u64,
diff --git a/rust/src/types.rs b/rust/src/types.rs
index d841096c5..f454e33ed 100644
--- a/rust/src/types.rs
+++ b/rust/src/types.rs
@@ -1113,7 +1113,7 @@ pub struct SessionConfig {
/// Canvas declarations this connection provides to the runtime.
pub canvases: Option>,
/// Provider-side canvas lifecycle handler. The SDK routes inbound
- /// `canvas.open` / `canvas.close` / `canvas.action.invoke` requests to
+ /// `canvas.open` / `canvas.close` / `canvas.invokeAction` requests to
/// this handler. Use [`with_canvas_handler`](Self::with_canvas_handler)
/// to install one.
pub canvas_handler: Option>,
diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs
index 09ece6cf5..12863aff4 100644
--- a/rust/tests/e2e.rs
+++ b/rust/tests/e2e.rs
@@ -7,6 +7,8 @@ mod abort;
mod ask_user;
#[path = "e2e/builtin_tools.rs"]
mod builtin_tools;
+#[path = "e2e/canvas.rs"]
+mod canvas;
#[path = "e2e/client.rs"]
mod client;
#[path = "e2e/client_api.rs"]
diff --git a/rust/tests/e2e/canvas.rs b/rust/tests/e2e/canvas.rs
new file mode 100644
index 000000000..1defe2c11
--- /dev/null
+++ b/rust/tests/e2e/canvas.rs
@@ -0,0 +1,275 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use github_copilot_sdk::canvas::{CanvasDeclaration, CanvasHandler, CanvasResult};
+use github_copilot_sdk::generated::api_types::{
+ CanvasAction, CanvasProviderCloseRequest, CanvasProviderInvokeActionRequest,
+ CanvasProviderOpenRequest, CanvasProviderOpenResult,
+};
+use github_copilot_sdk::types::ExtensionInfo;
+use parking_lot::Mutex;
+use serde_json::{Value, json};
+
+use super::support::with_e2e_context;
+
+struct TestCanvasHandler {
+ open_calls: Mutex>,
+ close_calls: Mutex>,
+ action_calls: Mutex>,
+}
+
+impl TestCanvasHandler {
+ fn new() -> Self {
+ Self {
+ open_calls: Mutex::new(Vec::new()),
+ close_calls: Mutex::new(Vec::new()),
+ action_calls: Mutex::new(Vec::new()),
+ }
+ }
+}
+
+#[async_trait]
+impl CanvasHandler for TestCanvasHandler {
+ async fn on_open(&self, ctx: CanvasProviderOpenRequest) -> CanvasResult {
+ self.open_calls.lock().push(ctx.clone());
+ Ok(CanvasProviderOpenResult {
+ url: Some(format!(
+ "https://example.com/counter/{}",
+ ctx.instance_id
+ )),
+ title: Some(format!("Counter {}", ctx.instance_id)),
+ status: Some("ready".to_string()),
+ })
+ }
+
+ async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult {
+ self.action_calls.lock().push(ctx.clone());
+ Ok(json!({ "newValue": 42 }))
+ }
+
+ async fn on_close(&self, ctx: CanvasProviderCloseRequest) -> CanvasResult<()> {
+ self.close_calls.lock().push(ctx.clone());
+ Ok(())
+ }
+}
+
+fn canvas_session_config(
+ ctx: &super::support::E2eContext,
+ handler: Arc,
+) -> github_copilot_sdk::types::SessionConfig {
+ let mut decl = CanvasDeclaration::new("counter", "Counter", "Tracks a counter value.");
+ decl.actions = Some(vec![CanvasAction {
+ name: "increment".to_string(),
+ description: Some("Increments the counter.".to_string()),
+ input_schema: None,
+ }]);
+
+ ctx.approve_all_session_config()
+ .with_request_canvas_renderer(true)
+ .with_request_extensions(true)
+ .with_extension_info(ExtensionInfo::new("rust-sdk-tests", "canvas-provider"))
+ .with_canvases([decl])
+ .with_canvas_handler(handler)
+}
+
+#[tokio::test]
+async fn canvas_list_discovers_declared_canvases() {
+ with_e2e_context("canvas", "canvas_list_discovers_declared_canvases", |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let client = ctx.start_client().await;
+ let handler = Arc::new(TestCanvasHandler::new());
+ let session = client
+ .create_session(canvas_session_config(&ctx, handler))
+ .await
+ .expect("create session");
+
+ let result = session.rpc().canvas().list().await.expect("list canvases");
+
+ assert_eq!(result.canvases.len(), 1);
+ assert_eq!(result.canvases[0].canvas_id, "counter");
+ assert_eq!(result.canvases[0].display_name, "Counter");
+ assert_eq!(result.canvases[0].description, "Tracks a counter value.");
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ })
+ .await;
+}
+
+#[tokio::test]
+async fn canvas_open_round_trip() {
+ with_e2e_context("canvas", "canvas_open_round_trip", |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let client = ctx.start_client().await;
+ let handler = Arc::new(TestCanvasHandler::new());
+ let session = client
+ .create_session(canvas_session_config(&ctx, handler.clone()))
+ .await
+ .expect("create session");
+
+ let canvas_list = session.rpc().canvas().list().await.expect("list canvases");
+ let canvas = &canvas_list.canvases[0];
+
+ let open_result = session
+ .rpc()
+ .canvas()
+ .open(github_copilot_sdk::generated::api_types::CanvasOpenRequest {
+ canvas_id: "counter".to_string(),
+ instance_id: "counter-1".to_string(),
+ extension_id: Some(canvas.extension_id.clone()),
+ input: Some(json!({ "start": 41 })),
+ })
+ .await
+ .expect("open canvas");
+
+ assert_eq!(open_result.instance_id, "counter-1");
+ assert_eq!(
+ open_result.title.as_deref(),
+ Some("Counter counter-1")
+ );
+ assert_eq!(open_result.status.as_deref(), Some("ready"));
+ assert_eq!(
+ open_result.url.as_deref(),
+ Some("https://example.com/counter/counter-1")
+ );
+
+ let opens = handler.open_calls.lock();
+ assert_eq!(opens.len(), 1);
+ assert_eq!(opens[0].canvas_id, "counter");
+ assert_eq!(opens[0].instance_id, "counter-1");
+ drop(opens);
+
+ let open_list = session
+ .rpc()
+ .canvas()
+ .list_open()
+ .await
+ .expect("list open canvases");
+ assert_eq!(open_list.open_canvases.len(), 1);
+ assert_eq!(open_list.open_canvases[0].instance_id, "counter-1");
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ })
+ .await;
+}
+
+#[tokio::test]
+async fn canvas_invoke_action_round_trip() {
+ with_e2e_context("canvas", "canvas_invoke_action_round_trip", |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let client = ctx.start_client().await;
+ let handler = Arc::new(TestCanvasHandler::new());
+ let session = client
+ .create_session(canvas_session_config(&ctx, handler.clone()))
+ .await
+ .expect("create session");
+
+ let canvas_list = session.rpc().canvas().list().await.expect("list canvases");
+ let canvas = &canvas_list.canvases[0];
+
+ session
+ .rpc()
+ .canvas()
+ .open(github_copilot_sdk::generated::api_types::CanvasOpenRequest {
+ canvas_id: "counter".to_string(),
+ instance_id: "counter-2".to_string(),
+ extension_id: Some(canvas.extension_id.clone()),
+ input: Some(json!({})),
+ })
+ .await
+ .expect("open canvas");
+
+ let result = session
+ .rpc()
+ .canvas()
+ .invoke_action(
+ github_copilot_sdk::generated::api_types::CanvasInvokeActionRequest {
+ instance_id: "counter-2".to_string(),
+ action_name: "increment".to_string(),
+ input: Some(json!({ "delta": 1 })),
+ },
+ )
+ .await
+ .expect("invoke action");
+
+ assert_eq!(result.result, Some(json!({ "newValue": 42 })));
+
+ let actions = handler.action_calls.lock();
+ assert_eq!(actions.len(), 1);
+ assert_eq!(actions[0].canvas_id, "counter");
+ assert_eq!(actions[0].instance_id, "counter-2");
+ assert_eq!(actions[0].action_name, "increment");
+ assert_eq!(actions[0].input, Some(json!({ "delta": 1 })));
+ drop(actions);
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ })
+ .await;
+}
+
+#[tokio::test]
+async fn canvas_close_round_trip() {
+ with_e2e_context("canvas", "canvas_close_round_trip", |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let client = ctx.start_client().await;
+ let handler = Arc::new(TestCanvasHandler::new());
+ let session = client
+ .create_session(canvas_session_config(&ctx, handler.clone()))
+ .await
+ .expect("create session");
+
+ let canvas_list = session.rpc().canvas().list().await.expect("list canvases");
+ let canvas = &canvas_list.canvases[0];
+
+ session
+ .rpc()
+ .canvas()
+ .open(github_copilot_sdk::generated::api_types::CanvasOpenRequest {
+ canvas_id: "counter".to_string(),
+ instance_id: "counter-3".to_string(),
+ extension_id: Some(canvas.extension_id.clone()),
+ input: Some(json!({})),
+ })
+ .await
+ .expect("open canvas");
+
+ assert!(handler.close_calls.lock().is_empty());
+
+ session
+ .rpc()
+ .canvas()
+ .close(github_copilot_sdk::generated::api_types::CanvasCloseRequest {
+ instance_id: "counter-3".to_string(),
+ })
+ .await
+ .expect("close canvas");
+
+ let closes = handler.close_calls.lock();
+ assert_eq!(closes.len(), 1);
+ assert_eq!(closes[0].canvas_id, "counter");
+ assert_eq!(closes[0].instance_id, "counter-3");
+ drop(closes);
+
+ let open_list = session
+ .rpc()
+ .canvas()
+ .list_open()
+ .await
+ .expect("list open canvases");
+ assert!(open_list.open_canvases.is_empty());
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ })
+ .await;
+}
diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs
index 050c5898d..322cc5fbb 100644
--- a/rust/tests/session_test.rs
+++ b/rust/tests/session_test.rs
@@ -6,11 +6,11 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use async_trait::async_trait;
-use github_copilot_sdk::canvas::{
- CanvasActionContext, CanvasDeclaration, CanvasHandler, CanvasOpenContext, CanvasOpenResponse,
- CanvasResult,
+use github_copilot_sdk::canvas::{CanvasDeclaration, CanvasHandler, CanvasResult};
+use github_copilot_sdk::generated::api_types::{
+ CanvasInstanceAvailability, CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest,
+ CanvasProviderOpenResult, OpenCanvasInstance,
};
-use github_copilot_sdk::generated::api_types::{CanvasInstanceAvailability, OpenCanvasInstance};
use github_copilot_sdk::handler::{
ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler,
ExitPlanModeHandler, ExitPlanModeResult, UserInputHandler, UserInputResponse,
@@ -31,15 +31,15 @@ struct TestCanvasHandler;
#[async_trait]
impl CanvasHandler for TestCanvasHandler {
- async fn on_open(&self, ctx: CanvasOpenContext) -> CanvasResult {
- Ok(CanvasOpenResponse {
+ async fn on_open(&self, ctx: CanvasProviderOpenRequest) -> CanvasResult {
+ Ok(CanvasProviderOpenResult {
url: Some(format!("https://example.test/{}", ctx.canvas_id)),
title: Some("Test Canvas".to_string()),
status: Some("ready".to_string()),
})
}
- async fn on_action(&self, ctx: CanvasActionContext) -> CanvasResult {
+ async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult {
Ok(serde_json::json!({
"actionName": ctx.action_name,
"input": ctx.input,
@@ -380,7 +380,7 @@ async fn provider_canvas_dispatch_routes_direct_canvas_action_requests() {
server
.send_request(
42,
- "canvas.action.invoke",
+ "canvas.invokeAction",
serde_json::json!({
"sessionId": session.id(),
"extensionId": "project:counter",
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index 13a7a1417..883895cde 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -1452,12 +1452,14 @@ function getCSharpSchemaTypeName(schema: JSONSchema7 | null | undefined, fallbac
return getRpcSchemaTypeName(schema, fallback);
}
-/** Returns the C# type for a method's result, accounting for nullable anyOf wrappers. */
+/** Returns the C# type for a method's result, accounting for nullable anyOf wrappers and opaque JSON. */
function resolvedResultTypeName(method: RpcMethod): string {
const schema = getMethodResultSchema(method);
if (!schema) return resultTypeName(method);
+ if (isOpaqueJson(schema)) return "object";
const inner = getNullableInner(schema);
if (inner) {
+ if (isOpaqueJson(inner)) return "object?";
// Nullable wrapper: resolve the inner $ref type name with "?" suffix
const innerName = inner.$ref
? typeToClassName(refTypeName(inner.$ref, rpcDefinitions))
@@ -2181,7 +2183,7 @@ function emitClientSessionApiRegistration(clientSchema: Record,
for (const { methods } of groups) {
for (const method of methods) {
const resultSchema = getMethodResultSchema(method);
- if (!isVoidSchema(resultSchema)) {
+ if (!isVoidSchema(resultSchema) && !isOpaqueJson(resultSchema)) {
emitRpcResultType(resultTypeName(method), resultSchema!, "public", classes);
}
diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts
index 49e537d8e..a8b85dc0b 100644
--- a/scripts/codegen/go.ts
+++ b/scripts/codegen/go.ts
@@ -34,6 +34,7 @@ import {
isIntegerSchemaBoundedToInt32,
isNodeFullyDeprecated,
isNodeFullyExperimental,
+ isOpaqueJson,
isRpcMethod,
isSchemaDeprecated,
isSchemaExperimental,
@@ -3559,6 +3560,8 @@ async function generateRpc(schemaPath?: string): Promise {
if (nullableInner) {
// Nullable results (e.g., *SessionFSError) don't need a wrapper type;
// the inner type is already in definitions via shared hoisting.
+ } else if (isOpaqueJson(resultSchema)) {
+ // Opaque JSON results map to `any` — no named struct needed.
} else if (isVoidSchema(resultSchema)) {
// Emit an empty struct for void results (forward-compatible with adding fields later)
allDefinitions[goResultTypeName(method)] = {
@@ -4035,10 +4038,15 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record<
}
const paramsType = resolveType(goParamsTypeName(method));
const nullableInner = resultSchema ? getNullableInner(resultSchema) : undefined;
- const resultType = nullableInner
- ? resolveType(goNullableResultTypeName(method, nullableInner))
- : resolveType(goResultTypeName(method));
- const returnType = unionInfos.has(resultType) ? resultType : `*${resultType}`;
+ let returnType: string;
+ if (isOpaqueJson(resultSchema)) {
+ returnType = "any";
+ } else {
+ const resultType = nullableInner
+ ? resolveType(goNullableResultTypeName(method, nullableInner))
+ : resolveType(goResultTypeName(method));
+ returnType = unionInfos.has(resultType) ? resultType : `*${resultType}`;
+ }
lines.push(`\t${clientHandlerMethodName(method.rpcMethod)}(request *${paramsType}) (${returnType}, error)`);
}
lines.push(`}`);
diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts
index 1af315eac..77bbb4813 100644
--- a/scripts/codegen/python.ts
+++ b/scripts/codegen/python.ts
@@ -18,6 +18,7 @@ import {
getRpcSchemaTypeName,
getSessionEventsSchemaPath,
isObjectSchema,
+ isOpaqueJson,
isVoidSchema,
getNullableInner,
isRpcMethod,
@@ -3225,7 +3226,8 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession:
const effectiveResultSchema = nullableInner ?? resultSchema;
const hasResult = !isVoidSchema(resultSchema) && !nullableInner;
const hasNullableResult = !!nullableInner;
- const resultIsObject = isPythonObjectResultSchema(effectiveResultSchema);
+ const resultIsOpaque = isOpaqueJson(effectiveResultSchema);
+ const resultIsObject = !resultIsOpaque && isPythonObjectResultSchema(effectiveResultSchema);
let resultType: string;
if (hasNullableResult) {
@@ -3264,7 +3266,11 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession:
// Deserialize helper
const innerTypeName = hasNullableResult ? resolveType(pythonResultTypeName(method, nullableInner)) : resultType;
+ const isAnyType = innerTypeName === "Any";
const deserialize = (expr: string) => {
+ if (resultIsOpaque || isAnyType) {
+ return expr;
+ }
if (hasNullableResult) {
return resultIsObject
? `${innerTypeName}.from_dict(${expr}) if ${expr} is not None else None`
diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts
index 61e551d68..f3a4bd192 100644
--- a/scripts/codegen/typescript.ts
+++ b/scripts/codegen/typescript.ts
@@ -846,6 +846,9 @@ function emitClientSessionApiRegistration(clientSchema: Record)
const groupExperimental = isNodeFullyExperimental(clientSchema[groupName] as Record);
if (groupDeprecated) {
lines.push(`/** @deprecated Handler for \`${groupName}\` client session API methods. */`);
+ } else if (groupExperimental) {
+ lines.push(`/** Handler for \`${groupName}\` client session API methods. */`);
+ lines.push(TS_EXPERIMENTAL_JSDOC);
} else {
lines.push(`/** Handler for \`${groupName}\` client session API methods. */`);
}
diff --git a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml
new file mode 100644
index 000000000..056351ddb
--- /dev/null
+++ b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml
@@ -0,0 +1,3 @@
+models:
+ - claude-sonnet-4.5
+conversations: []