From 334df7c4f8b79590217c676816b3e3fd4e659459 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Mon, 12 Oct 2026 15:07:17 -0700
Subject: [PATCH 01/12] canvas: add discovery and open commands
Add capability-gated exact-chat listCanvasTypes and openCanvas commands, including owning extension metadata, and regenerate all protocol clients and schemas.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
---
.../Generated/Commands.generated.cs | 57 ++++++++
.../JsonSerializerContext.generated.cs | 5 +
.../Generated/State.generated.cs | 101 ++++++++++++++
clients/go/ahptypes/commands.generated.go | 45 ++++++
clients/go/ahptypes/state.generated.go | 44 ++++++
.../generated/Commands.generated.kt | 48 +++++++
.../generated/State.generated.kt | 82 +++++++++++
clients/rust/crates/ahp-types/src/commands.rs | 62 ++++++++-
clients/rust/crates/ahp-types/src/state.rs | 82 +++++++++++
.../Generated/Commands.generated.swift | 68 +++++++++
.../Generated/State.generated.swift | 86 ++++++++++++
.../20261012-canvas-discovery-commands.json | 4 +
schema/actions.schema.json | 63 +++++++++
schema/commands.schema.json | 130 ++++++++++++++++++
schema/errors.schema.json | 130 ++++++++++++++++++
schema/notifications.schema.json | 63 +++++++++
schema/state.schema.json | 63 +++++++++
scripts/generate-csharp.ts | 4 +-
scripts/generate-go.ts | 4 +-
scripts/generate-kotlin.ts | 5 +-
scripts/generate-rust.ts | 6 +-
scripts/generate-swift.ts | 5 +-
types/channels-canvas/commands.ts | 74 ++++++++++
types/channels-canvas/state.ts | 47 +++++++
types/channels-root/state.ts | 6 +
types/commands.ts | 1 +
types/common/messages.ts | 7 +
types/version/message-checks.ts | 2 +
28 files changed, 1282 insertions(+), 12 deletions(-)
create mode 100644 docs/.changes/20261012-canvas-discovery-commands.json
create mode 100644 types/channels-canvas/commands.ts
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 54c918b1f..a6acf50fb 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1147,6 +1147,63 @@ public sealed record MoveChatResult
public required string Session { get; init; }
}
+/// Lists canvas types currently available to open for one exact chat.
+///
+/// This read MAY lazily resume an existing persisted chat so its extension
+/// registry can be reconstructed. It MUST NOT create a backing runtime for a
+/// brand-new chat, send a user message, or invoke a model.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+public sealed record ListCanvasTypesParams
+{
+ /// Exact chat URI whose extension registry should be queried.
+ public required string Channel { get; init; }
+
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ [JsonPropertyName("_meta")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Meta { get; init; }
+}
+
+/// Canvas types available to open for the requested chat.
+public sealed record ListCanvasTypesResult
+{
+ /// Available canvas types and their declaring extensions.
+ public required List Items { get; init; }
+}
+
+/// Opens or focuses one stable canvas instance in an exact chat.
+///
+/// The resulting live instance is published through the chat's
+/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
+/// Repeating this command with the same `instanceId` focuses or refreshes that
+/// logical instance rather than creating a duplicate.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+public sealed record OpenCanvasParams
+{
+ /// Exact chat URI that owns the canvas instance.
+ public required string Channel { get; init; }
+
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ [JsonPropertyName("_meta")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Meta { get; init; }
+
+ /// Stable identifier of the extension that declares the canvas.
+ public required string ExtensionId { get; init; }
+
+ /// Provider-local canvas type identifier.
+ public required string CanvasId { get; init; }
+
+ /// Caller-chosen stable instance identifier.
+ public required string InstanceId { get; init; }
+}
+
/// Returns a list of session summaries. Used to populate session lists and sidebars.
///
/// The session list is **not** part of the state tree because it can be arbitrarily
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index b9d54208f..898b036ba 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -83,9 +83,11 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(BackgroundSubagentWork))]
[JsonSerializable(typeof(BackgroundWork))]
[JsonSerializable(typeof(BackgroundWorkKind))]
+[JsonSerializable(typeof(CanvasExtensionSource))]
[JsonSerializable(typeof(CanvasReference))]
[JsonSerializable(typeof(CanvasState))]
[JsonSerializable(typeof(CanvasStateChangedAction))]
+[JsonSerializable(typeof(CanvasTypeDeclaration))]
[JsonSerializable(typeof(Changeset))]
[JsonSerializable(typeof(ChangesetCapabilities))]
[JsonSerializable(typeof(ChangesetClearedAction))]
@@ -259,6 +261,8 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(JsonRpcSuccessResponse))]
[JsonSerializable(typeof(ListAutomationTriggerDefinitionsParams))]
[JsonSerializable(typeof(ListAutomationTriggerDefinitionsResult))]
+[JsonSerializable(typeof(ListCanvasTypesParams))]
+[JsonSerializable(typeof(ListCanvasTypesResult))]
[JsonSerializable(typeof(ListSessionsParams))]
[JsonSerializable(typeof(ListSessionsResult))]
[JsonSerializable(typeof(MarkdownResponsePart))]
@@ -290,6 +294,7 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(MultipleChatProvidersCapability))]
[JsonSerializable(typeof(MultipleChatsCapability))]
[JsonSerializable(typeof(MultipleWorkingDirectoriesCapability))]
+[JsonSerializable(typeof(OpenCanvasParams))]
[JsonSerializable(typeof(OtlpExportLogsParams))]
[JsonSerializable(typeof(OtlpExportMetricsParams))]
[JsonSerializable(typeof(OtlpExportTracesParams))]
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 427076b09..525ff95d0 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -176,6 +176,68 @@ public override void Write(Utf8JsonWriter writer, SessionOriginKind value, JsonS
=> writer.WriteStringValue(value.Value);
}
+/// Where the runtime discovered an extension that declares a canvas type.
+[JsonConverter(typeof(CanvasExtensionSourceConverter))]
+public readonly struct CanvasExtensionSource : IEquatable
+{
+ private readonly string? _value;
+
+ /// Wraps a raw wire value — including one this build does not recognize.
+ /// The raw wire string.
+ public CanvasExtensionSource(string value)
+ {
+ _value = value;
+ }
+
+ /// The raw wire value.
+ public string Value => _value ?? string.Empty;
+
+ /// Extension installed for the current user.
+ public static readonly CanvasExtensionSource User = new CanvasExtensionSource("user");
+
+ /// Extension discovered in the current project.
+ public static readonly CanvasExtensionSource Project = new CanvasExtensionSource("project");
+
+ /// Extension installed only for the current session.
+ public static readonly CanvasExtensionSource Session = new CanvasExtensionSource("session");
+
+ /// Extension contributed by an installed plugin.
+ public static readonly CanvasExtensionSource Plugin = new CanvasExtensionSource("plugin");
+
+ /// Extension source is unavailable or not recognized by this protocol version.
+ public static readonly CanvasExtensionSource Unspecified = new CanvasExtensionSource("unknown");
+
+ ///
+ public bool Equals(CanvasExtensionSource other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
+
+ ///
+ public override bool Equals(object? obj) => obj is CanvasExtensionSource other && Equals(other);
+
+ ///
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
+
+ ///
+ public override string ToString() => Value;
+
+ /// Ordinal equality over the raw wire value.
+ public static bool operator ==(CanvasExtensionSource left, CanvasExtensionSource right) => left.Equals(right);
+
+ /// Ordinal inequality over the raw wire value.
+ public static bool operator !=(CanvasExtensionSource left, CanvasExtensionSource right) => !left.Equals(right);
+}
+
+/// Reads and writes as its raw wire string, preserving unrecognized values.
+internal sealed class CanvasExtensionSourceConverter : JsonConverter
+{
+ ///
+ public override CanvasExtensionSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => new CanvasExtensionSource(reader.GetString() ?? throw new JsonException("CanvasExtensionSource expects a JSON string."));
+
+ ///
+ public override void Write(Utf8JsonWriter writer, CanvasExtensionSource value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.Value);
+}
+
/// Discriminant for {@link ChatOrigin} — how a chat came into existence.
[JsonConverter(typeof(ChatOriginKindConverter))]
public readonly struct ChatOriginKind : IEquatable
@@ -2053,6 +2115,12 @@ public sealed record AgentInfo
/// per-capability options.
public sealed record AgentCapabilities
{
+ /// The agent can list canvas types for an exact chat with `listCanvasTypes`
+ /// and open them with `openCanvas`. Live instances are published through the
+ /// chat's canvas references and their `ahp-canvas:` state channels.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Canvases { get; init; }
+
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
/// session starts with. An empty object `{}` advertises multi-chat without
@@ -2606,6 +2674,39 @@ public sealed class ChatState
public Dictionary? Meta { get; set; }
}
+/// One canvas type currently available to open for an exact chat.
+///
+/// This is discovery metadata, not a live canvas instance. Live instances are
+/// represented by {@link CanvasReference} entries on chat state and their
+/// corresponding {@link CanvasState} channels.
+public sealed record CanvasTypeDeclaration
+{
+ /// Provider-local canvas type identifier.
+ public required string CanvasId { get; init; }
+
+ /// Stable identifier of the extension that declares the canvas.
+ public required string ExtensionId { get; init; }
+
+ /// Where the runtime discovered the declaring extension.
+ public CanvasExtensionSource ExtensionSource { get; init; }
+
+ /// Human-readable extension name, when available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ExtensionName { get; init; }
+
+ /// Human-readable canvas type name.
+ public required string DisplayName { get; init; }
+
+ /// Description of what the canvas type does.
+ public required string Description { get; init; }
+
+ /// Whether opening this canvas requires an input flow.
+ public bool RequiresInput { get; init; }
+
+ /// Number of actions the canvas declares.
+ public long ActionCount { get; init; }
+}
+
/// Stable reference to a subscribable canvas channel.
///
/// Chat state intentionally carries only this reference so the experimental
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 332dc0027..ecf4a6743 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -764,6 +764,51 @@ type MoveChatResult struct {
Session URI `json:"session"`
}
+// Lists canvas types currently available to open for one exact chat.
+//
+// This read MAY lazily resume an existing persisted chat so its extension
+// registry can be reconstructed. It MUST NOT create a backing runtime for a
+// brand-new chat, send a user message, or invoke a model.
+//
+// Clients MUST NOT call this command unless the selected agent advertises
+// {@link AgentCapabilities.canvases}.
+type ListCanvasTypesParams struct {
+ // Channel URI this command targets.
+ Channel URI `json:"channel"`
+ // Optional JSON-serializable metadata associated with this request.
+ // Receivers MUST ignore keys they do not understand.
+ Meta map[string]json.RawMessage `json:"_meta,omitempty"`
+}
+
+// Canvas types available to open for the requested chat.
+type ListCanvasTypesResult struct {
+ // Available canvas types and their declaring extensions.
+ Items []CanvasTypeDeclaration `json:"items"`
+}
+
+// Opens or focuses one stable canvas instance in an exact chat.
+//
+// The resulting live instance is published through the chat's
+// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
+// Repeating this command with the same `instanceId` focuses or refreshes that
+// logical instance rather than creating a duplicate.
+//
+// Clients MUST NOT call this command unless the selected agent advertises
+// {@link AgentCapabilities.canvases}.
+type OpenCanvasParams struct {
+ // Channel URI this command targets.
+ Channel URI `json:"channel"`
+ // Optional JSON-serializable metadata associated with this request.
+ // Receivers MUST ignore keys they do not understand.
+ Meta map[string]json.RawMessage `json:"_meta,omitempty"`
+ // Stable identifier of the extension that declares the canvas.
+ ExtensionId string `json:"extensionId"`
+ // Provider-local canvas type identifier.
+ CanvasId string `json:"canvasId"`
+ // Caller-chosen stable instance identifier.
+ InstanceId string `json:"instanceId"`
+}
+
// Returns a list of session summaries. Used to populate session lists and sidebars.
//
// The session list is **not** part of the state tree because it can be arbitrarily
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index 6f48cb83c..a1526c0ff 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -503,6 +503,22 @@ const (
SessionOriginKindAutomation SessionOriginKind = "automation"
)
+// Where the runtime discovered an extension that declares a canvas type.
+type CanvasExtensionSource string
+
+const (
+ // Extension installed for the current user.
+ CanvasExtensionSourceUser CanvasExtensionSource = "user"
+ // Extension discovered in the current project.
+ CanvasExtensionSourceProject CanvasExtensionSource = "project"
+ // Extension installed only for the current session.
+ CanvasExtensionSourceSession CanvasExtensionSource = "session"
+ // Extension contributed by an installed plugin.
+ CanvasExtensionSourcePlugin CanvasExtensionSource = "plugin"
+ // Extension source is unavailable or not recognized by this protocol version.
+ CanvasExtensionSourceUnspecified CanvasExtensionSource = "unknown"
+)
+
// Operations the host currently permits for an automation.
//
// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -786,6 +802,10 @@ type AgentInfo struct {
// corresponding client commands MUST NOT be used. Sub-fields carry
// per-capability options.
type AgentCapabilities struct {
+ // The agent can list canvas types for an exact chat with `listCanvasTypes`
+ // and open them with `openCanvas`. Live instances are published through the
+ // chat's canvas references and their `ahp-canvas:` state channels.
+ Canvases map[string]json.RawMessage `json:"canvases,omitempty"`
// The agent can host more than one concurrent chat per session. When absent,
// clients MUST NOT call `createChat` to open chats beyond the default one the
// session starts with. An empty object `{}` advertises multi-chat without
@@ -1480,6 +1500,30 @@ type ChatState struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
+// One canvas type currently available to open for an exact chat.
+//
+// This is discovery metadata, not a live canvas instance. Live instances are
+// represented by {@link CanvasReference} entries on chat state and their
+// corresponding {@link CanvasState} channels.
+type CanvasTypeDeclaration struct {
+ // Provider-local canvas type identifier.
+ CanvasId string `json:"canvasId"`
+ // Stable identifier of the extension that declares the canvas.
+ ExtensionId string `json:"extensionId"`
+ // Where the runtime discovered the declaring extension.
+ ExtensionSource CanvasExtensionSource `json:"extensionSource"`
+ // Human-readable extension name, when available.
+ ExtensionName *string `json:"extensionName,omitempty"`
+ // Human-readable canvas type name.
+ DisplayName string `json:"displayName"`
+ // Description of what the canvas type does.
+ Description string `json:"description"`
+ // Whether opening this canvas requires an input flow.
+ RequiresInput bool `json:"requiresInput"`
+ // Number of actions the canvas declares.
+ ActionCount int64 `json:"actionCount"`
+}
+
// Stable reference to a subscribable canvas channel.
//
// Chat state intentionally carries only this reference so the experimental
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index 76e2ba4f2..4e85b99ad 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1024,6 +1024,54 @@ data class MoveChatResult(
val session: String
)
+@Serializable
+data class ListCanvasTypesParams(
+ /**
+ * Channel URI this command targets.
+ */
+ val channel: String,
+ /**
+ * Optional JSON-serializable metadata associated with this request.
+ * Receivers MUST ignore keys they do not understand.
+ */
+ @SerialName("_meta")
+ val meta: Map? = null
+)
+
+@Serializable
+data class ListCanvasTypesResult(
+ /**
+ * Available canvas types and their declaring extensions.
+ */
+ val items: List
+)
+
+@Serializable
+data class OpenCanvasParams(
+ /**
+ * Channel URI this command targets.
+ */
+ val channel: String,
+ /**
+ * Optional JSON-serializable metadata associated with this request.
+ * Receivers MUST ignore keys they do not understand.
+ */
+ @SerialName("_meta")
+ val meta: Map? = null,
+ /**
+ * Stable identifier of the extension that declares the canvas.
+ */
+ val extensionId: String,
+ /**
+ * Provider-local canvas type identifier.
+ */
+ val canvasId: String,
+ /**
+ * Caller-chosen stable instance identifier.
+ */
+ val instanceId: String
+)
+
@Serializable
data class ListSessionsParams(
/**
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 6b86957b0..607c36eb0 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1066,6 +1066,46 @@ internal object SessionOriginKindSerializer : KSerializer {
SessionOriginKind(decoder.decodeString())
}
+/**
+ * Where the runtime discovered an extension that declares a canvas type.
+ */
+@Serializable(with = CanvasExtensionSourceSerializer::class)
+@JvmInline
+value class CanvasExtensionSource(val rawValue: String) {
+ companion object {
+ /**
+ * Extension installed for the current user.
+ */
+ val USER: CanvasExtensionSource = CanvasExtensionSource("user")
+ /**
+ * Extension discovered in the current project.
+ */
+ val PROJECT: CanvasExtensionSource = CanvasExtensionSource("project")
+ /**
+ * Extension installed only for the current session.
+ */
+ val SESSION: CanvasExtensionSource = CanvasExtensionSource("session")
+ /**
+ * Extension contributed by an installed plugin.
+ */
+ val PLUGIN: CanvasExtensionSource = CanvasExtensionSource("plugin")
+ /**
+ * Extension source is unavailable or not recognized by this protocol version.
+ */
+ val UNSPECIFIED: CanvasExtensionSource = CanvasExtensionSource("unknown")
+ }
+}
+
+internal object CanvasExtensionSourceSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("CanvasExtensionSource", PrimitiveKind.STRING)
+ override fun serialize(encoder: Encoder, value: CanvasExtensionSource) {
+ encoder.encodeString(value.rawValue)
+ }
+ override fun deserialize(decoder: Decoder): CanvasExtensionSource =
+ CanvasExtensionSource(decoder.decodeString())
+}
+
/**
* Operations the host currently permits for an automation.
*
@@ -1507,6 +1547,12 @@ data class AgentInfo(
@Serializable
data class AgentCapabilities(
+ /**
+ * The agent can list canvas types for an exact chat with `listCanvasTypes`
+ * and open them with `openCanvas`. Live instances are published through the
+ * chat's canvas references and their `ahp-canvas:` state channels.
+ */
+ val canvases: Map? = null,
/**
* The agent can host more than one concurrent chat per session. When absent,
* clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -2241,6 +2287,42 @@ data class BackgroundSubagentWork(
val chat: String
)
+@Serializable
+data class CanvasTypeDeclaration(
+ /**
+ * Provider-local canvas type identifier.
+ */
+ val canvasId: String,
+ /**
+ * Stable identifier of the extension that declares the canvas.
+ */
+ val extensionId: String,
+ /**
+ * Where the runtime discovered the declaring extension.
+ */
+ val extensionSource: CanvasExtensionSource,
+ /**
+ * Human-readable extension name, when available.
+ */
+ val extensionName: String? = null,
+ /**
+ * Human-readable canvas type name.
+ */
+ val displayName: String,
+ /**
+ * Description of what the canvas type does.
+ */
+ val description: String,
+ /**
+ * Whether opening this canvas requires an input flow.
+ */
+ val requiresInput: Boolean,
+ /**
+ * Number of actions the canvas declares.
+ */
+ val actionCount: Long
+)
+
@Serializable
data class CanvasReference(
/**
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 5be59736f..1d5d092e8 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -16,10 +16,11 @@ use crate::actions::{ActionEnvelope, StateAction};
#[allow(unused_imports)]
use crate::state::{
AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate,
- AutomationTrigger, AutomationTriggerDefinition, ChannelFlowControl, ChannelReceiveLimits,
- ContentRef, Message, MessageAttachment, ModelSelection, ResumedChannelSubscription,
- SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot,
- SnapshotState, TcpConnectionsCapability, TelemetryCapabilities, TerminalClaim, TextRange, Turn,
+ AutomationTrigger, AutomationTriggerDefinition, CanvasTypeDeclaration, ChannelFlowControl,
+ ChannelReceiveLimits, ContentRef, Message, MessageAttachment, ModelSelection,
+ ResumedChannelSubscription, SessionActiveClient, SessionConfigSchema, SessionSummary,
+ SideChatSelection, Snapshot, SnapshotState, TcpConnectionsCapability, TelemetryCapabilities,
+ TerminalClaim, TextRange, Turn,
};
// ─── Enums ────────────────────────────────────────────────────────────
@@ -1055,6 +1056,59 @@ pub struct MoveChatResult {
pub session: Uri,
}
+/// Lists canvas types currently available to open for one exact chat.
+///
+/// This read MAY lazily resume an existing persisted chat so its extension
+/// registry can be reconstructed. It MUST NOT create a backing runtime for a
+/// brand-new chat, send a user message, or invoke a model.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListCanvasTypesParams {
+ /// Channel URI this command targets.
+ pub channel: Uri,
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
+ pub meta: Option,
+}
+
+/// Canvas types available to open for the requested chat.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListCanvasTypesResult {
+ /// Available canvas types and their declaring extensions.
+ pub items: Vec,
+}
+
+/// Opens or focuses one stable canvas instance in an exact chat.
+///
+/// The resulting live instance is published through the chat's
+/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
+/// Repeating this command with the same `instanceId` focuses or refreshes that
+/// logical instance rather than creating a duplicate.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OpenCanvasParams {
+ /// Channel URI this command targets.
+ pub channel: Uri,
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
+ pub meta: Option,
+ /// Stable identifier of the extension that declares the canvas.
+ pub extension_id: String,
+ /// Provider-local canvas type identifier.
+ pub canvas_id: String,
+ /// Caller-chosen stable instance identifier.
+ pub instance_id: String,
+}
+
/// Returns a list of session summaries. Used to populate session lists and sidebars.
///
/// The session list is **not** part of the state tree because it can be arbitrarily
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index afa7eaefc..caf399802 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1354,6 +1354,56 @@ impl<'de> serde::Deserialize<'de> for SessionOriginKind {
}
}
+/// Where the runtime discovered an extension that declares a canvas type.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum CanvasExtensionSource {
+ /// Extension installed for the current user.
+ User,
+ /// Extension discovered in the current project.
+ Project,
+ /// Extension installed only for the current session.
+ Session,
+ /// Extension contributed by an installed plugin.
+ Plugin,
+ /// Extension source is unavailable or not recognized by this protocol version.
+ Unspecified,
+ /// Unknown raw value from a newer protocol version, preserved verbatim.
+ Unknown(String),
+}
+
+impl serde::Serialize for CanvasExtensionSource {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ match self {
+ Self::User => serializer.serialize_str("user"),
+ Self::Project => serializer.serialize_str("project"),
+ Self::Session => serializer.serialize_str("session"),
+ Self::Plugin => serializer.serialize_str("plugin"),
+ Self::Unspecified => serializer.serialize_str("unknown"),
+ Self::Unknown(value) => serializer.serialize_str(value),
+ }
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for CanvasExtensionSource {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let raw = ::deserialize(deserializer)?;
+ Ok(match raw.as_str() {
+ "user" => Self::User,
+ "project" => Self::Project,
+ "session" => Self::Session,
+ "plugin" => Self::Plugin,
+ "unknown" => Self::Unspecified,
+ _ => Self::Unknown(raw),
+ })
+ }
+}
+
/// Operations the host currently permits for an automation.
///
/// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -1773,6 +1823,11 @@ pub struct AgentInfo {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
+ /// The agent can list canvas types for an exact chat with `listCanvasTypes`
+ /// and open them with `openCanvas`. Live instances are published through the
+ /// chat's canvas references and their `ahp-canvas:` state channels.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub canvases: Option,
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
/// session starts with. An empty object `{}` advertises multi-chat without
@@ -2158,6 +2213,33 @@ pub struct ChatState {
pub meta: Option,
}
+/// One canvas type currently available to open for an exact chat.
+///
+/// This is discovery metadata, not a live canvas instance. Live instances are
+/// represented by {@link CanvasReference} entries on chat state and their
+/// corresponding {@link CanvasState} channels.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CanvasTypeDeclaration {
+ /// Provider-local canvas type identifier.
+ pub canvas_id: String,
+ /// Stable identifier of the extension that declares the canvas.
+ pub extension_id: String,
+ /// Where the runtime discovered the declaring extension.
+ pub extension_source: CanvasExtensionSource,
+ /// Human-readable extension name, when available.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub extension_name: Option,
+ /// Human-readable canvas type name.
+ pub display_name: String,
+ /// Description of what the canvas type does.
+ pub description: String,
+ /// Whether opening this canvas requires an input flow.
+ pub requires_input: bool,
+ /// Number of actions the canvas declares.
+ pub action_count: i64,
+}
+
/// Stable reference to a subscribable canvas channel.
///
/// Chat state intentionally carries only this reference so the experimental
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index 4d3853682..d33c34e4f 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1154,6 +1154,74 @@ public struct MoveChatResult: Codable, Sendable {
}
}
+public struct ListCanvasTypesParams: Codable, Sendable {
+ /// Channel URI this command targets.
+ public var channel: String
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ public var meta: [String: AnyCodable]?
+
+ enum CodingKeys: String, CodingKey {
+ case channel
+ case meta = "_meta"
+ }
+
+ public init(
+ channel: String,
+ meta: [String: AnyCodable]? = nil
+ ) {
+ self.channel = channel
+ self.meta = meta
+ }
+}
+
+public struct ListCanvasTypesResult: Codable, Sendable {
+ /// Available canvas types and their declaring extensions.
+ public var items: [CanvasTypeDeclaration]
+
+ public init(
+ items: [CanvasTypeDeclaration]
+ ) {
+ self.items = items
+ }
+}
+
+public struct OpenCanvasParams: Codable, Sendable {
+ /// Channel URI this command targets.
+ public var channel: String
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ public var meta: [String: AnyCodable]?
+ /// Stable identifier of the extension that declares the canvas.
+ public var extensionId: String
+ /// Provider-local canvas type identifier.
+ public var canvasId: String
+ /// Caller-chosen stable instance identifier.
+ public var instanceId: String
+
+ enum CodingKeys: String, CodingKey {
+ case channel
+ case meta = "_meta"
+ case extensionId
+ case canvasId
+ case instanceId
+ }
+
+ public init(
+ channel: String,
+ meta: [String: AnyCodable]? = nil,
+ extensionId: String,
+ canvasId: String,
+ instanceId: String
+ ) {
+ self.channel = channel
+ self.meta = meta
+ self.extensionId = extensionId
+ self.canvasId = canvasId
+ self.instanceId = instanceId
+ }
+}
+
public struct ListSessionsParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index 0d6fcf81d..9146fcde7 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1049,6 +1049,47 @@ public enum SessionOriginKind: Codable, Sendable, Equatable {
}
}
+/// Where the runtime discovered an extension that declares a canvas type.
+public enum CanvasExtensionSource: Codable, Sendable, Equatable {
+ /// Extension installed for the current user.
+ case user
+ /// Extension discovered in the current project.
+ case project
+ /// Extension installed only for the current session.
+ case session
+ /// Extension contributed by an installed plugin.
+ case plugin
+ /// Extension source is unavailable or not recognized by this protocol version.
+ case unspecified
+ /// Unknown raw value from a newer protocol version, preserved verbatim.
+ case unknown(String)
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let raw = try container.decode(String.self)
+ switch raw {
+ case "user": self = .user
+ case "project": self = .project
+ case "session": self = .session
+ case "plugin": self = .plugin
+ case "unknown": self = .unspecified
+ default: self = .unknown(raw)
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case .user: try container.encode("user")
+ case .project: try container.encode("project")
+ case .session: try container.encode("session")
+ case .plugin: try container.encode("plugin")
+ case .unspecified: try container.encode("unknown")
+ case .unknown(let raw): try container.encode(raw)
+ }
+ }
+}
+
/// Operations the host currently permits for an automation.
///
/// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -1479,6 +1520,10 @@ public struct AgentInfo: Codable, Sendable {
}
public struct AgentCapabilities: Codable, Sendable {
+ /// The agent can list canvas types for an exact chat with `listCanvasTypes`
+ /// and open them with `openCanvas`. Live instances are published through the
+ /// chat's canvas references and their `ahp-canvas:` state channels.
+ public var canvases: [String: AnyCodable]?
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
/// session starts with. An empty object `{}` advertises multi-chat without
@@ -1497,9 +1542,11 @@ public struct AgentCapabilities: Codable, Sendable {
public var multipleWorkingDirectories: MultipleWorkingDirectoriesCapability?
public init(
+ canvases: [String: AnyCodable]? = nil,
multipleChats: MultipleChatsCapability? = nil,
multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = nil
) {
+ self.canvases = canvases
self.multipleChats = multipleChats
self.multipleWorkingDirectories = multipleWorkingDirectories
}
@@ -6197,6 +6244,45 @@ public struct EmptyState: Codable, Sendable {
}
}
+public struct CanvasTypeDeclaration: Codable, Sendable {
+ /// Provider-local canvas type identifier.
+ public var canvasId: String
+ /// Stable identifier of the extension that declares the canvas.
+ public var extensionId: String
+ /// Where the runtime discovered the declaring extension.
+ public var extensionSource: CanvasExtensionSource
+ /// Human-readable extension name, when available.
+ public var extensionName: String?
+ /// Human-readable canvas type name.
+ public var displayName: String
+ /// Description of what the canvas type does.
+ public var description: String
+ /// Whether opening this canvas requires an input flow.
+ public var requiresInput: Bool
+ /// Number of actions the canvas declares.
+ public var actionCount: Int
+
+ public init(
+ canvasId: String,
+ extensionId: String,
+ extensionSource: CanvasExtensionSource,
+ extensionName: String? = nil,
+ displayName: String,
+ description: String,
+ requiresInput: Bool,
+ actionCount: Int
+ ) {
+ self.canvasId = canvasId
+ self.extensionId = extensionId
+ self.extensionSource = extensionSource
+ self.extensionName = extensionName
+ self.displayName = displayName
+ self.description = description
+ self.requiresInput = requiresInput
+ self.actionCount = actionCount
+ }
+}
+
public struct CanvasReference: Codable, Sendable {
/// Canvas channel URI. Subscribe to this resource for the full state.
public var resource: String
diff --git a/docs/.changes/20261012-canvas-discovery-commands.json b/docs/.changes/20261012-canvas-discovery-commands.json
new file mode 100644
index 000000000..7e113fe82
--- /dev/null
+++ b/docs/.changes/20261012-canvas-discovery-commands.json
@@ -0,0 +1,4 @@
+{
+ "type": "added",
+ "message": "Capability-gated `listCanvasTypes` and `openCanvas` commands for discovering extension canvases and opening them in an exact chat."
+}
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index 0e915cce3..7ba69625c 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -3338,6 +3338,11 @@
"type": "object",
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
+ "canvases": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ },
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
"description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding\ncreation mode."
@@ -7477,6 +7482,53 @@
"title"
]
},
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "extensionSource": {
+ "$ref": "#/$defs/CanvasExtensionSource",
+ "description": "Where the runtime discovered the declaring extension."
+ },
+ "extensionName": {
+ "type": "string",
+ "description": "Human-readable extension name, when available."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Human-readable canvas type name."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of what the canvas type does."
+ },
+ "requiresInput": {
+ "type": "boolean",
+ "description": "Whether opening this canvas requires an input flow."
+ },
+ "actionCount": {
+ "type": "number",
+ "description": "Number of actions the canvas declares."
+ }
+ },
+ "required": [
+ "canvasId",
+ "extensionId",
+ "extensionSource",
+ "displayName",
+ "description",
+ "requiresInput",
+ "actionCount"
+ ]
+ },
"CanvasReference": {
"type": "object",
"description": "Stable reference to a subscribable canvas channel.\n\nChat state intentionally carries only this reference so the experimental\ncanvas presentation model can evolve without changing the stable chat\nchannel shape.",
@@ -9821,6 +9873,17 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
+ "CanvasExtensionSource": {
+ "enum": [
+ "user",
+ "project",
+ "session",
+ "plugin",
+ "unknown"
+ ],
+ "type": "string",
+ "description": "Where the runtime discovered an extension that declares a canvas type."
+ },
"ChangesetOperationScope": {
"enum": [
"changeset",
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index 46e0b5cd4..e2cbbc0ed 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -1576,6 +1576,73 @@
"channel"
]
},
+ "ListCanvasTypesParams": {
+ "type": "object",
+ "description": "Lists canvas types currently available to open for one exact chat.\n\nThis read MAY lazily resume an existing persisted chat so its extension\nregistry can be reconstructed. It MUST NOT create a backing runtime for a\nbrand-new chat, send a user message, or invoke a model.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "properties": {
+ "channel": {
+ "$ref": "#/$defs/URI",
+ "description": "Exact chat URI whose extension registry should be queried."
+ },
+ "_meta": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
+ }
+ },
+ "required": [
+ "channel"
+ ]
+ },
+ "ListCanvasTypesResult": {
+ "type": "object",
+ "description": "Canvas types available to open for the requested chat.",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Available canvas types and their declaring extensions."
+ }
+ },
+ "required": [
+ "items"
+ ]
+ },
+ "OpenCanvasParams": {
+ "type": "object",
+ "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "properties": {
+ "channel": {
+ "$ref": "#/$defs/URI",
+ "description": "Exact chat URI that owns the canvas instance."
+ },
+ "_meta": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "instanceId": {
+ "type": "string",
+ "description": "Caller-chosen stable instance identifier."
+ }
+ },
+ "required": [
+ "channel",
+ "extensionId",
+ "canvasId",
+ "instanceId"
+ ]
+ },
"CreateTerminalParams": {
"type": "object",
"description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.",
@@ -2638,6 +2705,11 @@
"type": "object",
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
+ "canvases": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ },
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
"description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding\ncreation mode."
@@ -6777,6 +6849,53 @@
"title"
]
},
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "extensionSource": {
+ "$ref": "#/$defs/CanvasExtensionSource",
+ "description": "Where the runtime discovered the declaring extension."
+ },
+ "extensionName": {
+ "type": "string",
+ "description": "Human-readable extension name, when available."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Human-readable canvas type name."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of what the canvas type does."
+ },
+ "requiresInput": {
+ "type": "boolean",
+ "description": "Whether opening this canvas requires an input flow."
+ },
+ "actionCount": {
+ "type": "number",
+ "description": "Number of actions the canvas declares."
+ }
+ },
+ "required": [
+ "canvasId",
+ "extensionId",
+ "extensionSource",
+ "displayName",
+ "description",
+ "requiresInput",
+ "actionCount"
+ ]
+ },
"CanvasReference": {
"type": "object",
"description": "Stable reference to a subscribable canvas channel.\n\nChat state intentionally carries only this reference so the experimental\ncanvas presentation model can evolve without changing the stable chat\nchannel shape.",
@@ -11639,6 +11758,17 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
+ "CanvasExtensionSource": {
+ "enum": [
+ "user",
+ "project",
+ "session",
+ "plugin",
+ "unknown"
+ ],
+ "type": "string",
+ "description": "Where the runtime discovered an extension that declares a canvas type."
+ },
"TerminalLifecycleState": {
"oneOf": [
{
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index d42af0ade..846ddfc1e 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -712,6 +712,11 @@
"type": "object",
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
+ "canvases": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ },
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
"description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding\ncreation mode."
@@ -4851,6 +4856,53 @@
"title"
]
},
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "extensionSource": {
+ "$ref": "#/$defs/CanvasExtensionSource",
+ "description": "Where the runtime discovered the declaring extension."
+ },
+ "extensionName": {
+ "type": "string",
+ "description": "Human-readable extension name, when available."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Human-readable canvas type name."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of what the canvas type does."
+ },
+ "requiresInput": {
+ "type": "boolean",
+ "description": "Whether opening this canvas requires an input flow."
+ },
+ "actionCount": {
+ "type": "number",
+ "description": "Number of actions the canvas declares."
+ }
+ },
+ "required": [
+ "canvasId",
+ "extensionId",
+ "extensionSource",
+ "displayName",
+ "description",
+ "requiresInput",
+ "actionCount"
+ ]
+ },
"CanvasReference": {
"type": "object",
"description": "Stable reference to a subscribable canvas channel.\n\nChat state intentionally carries only this reference so the experimental\ncanvas presentation model can evolve without changing the stable chat\nchannel shape.",
@@ -7716,6 +7768,73 @@
"channel"
]
},
+ "ListCanvasTypesParams": {
+ "type": "object",
+ "description": "Lists canvas types currently available to open for one exact chat.\n\nThis read MAY lazily resume an existing persisted chat so its extension\nregistry can be reconstructed. It MUST NOT create a backing runtime for a\nbrand-new chat, send a user message, or invoke a model.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "properties": {
+ "channel": {
+ "$ref": "#/$defs/URI",
+ "description": "Exact chat URI whose extension registry should be queried."
+ },
+ "_meta": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
+ }
+ },
+ "required": [
+ "channel"
+ ]
+ },
+ "ListCanvasTypesResult": {
+ "type": "object",
+ "description": "Canvas types available to open for the requested chat.",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Available canvas types and their declaring extensions."
+ }
+ },
+ "required": [
+ "items"
+ ]
+ },
+ "OpenCanvasParams": {
+ "type": "object",
+ "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "properties": {
+ "channel": {
+ "$ref": "#/$defs/URI",
+ "description": "Exact chat URI that owns the canvas instance."
+ },
+ "_meta": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "instanceId": {
+ "type": "string",
+ "description": "Caller-chosen stable instance identifier."
+ }
+ },
+ "required": [
+ "channel",
+ "extensionId",
+ "canvasId",
+ "instanceId"
+ ]
+ },
"CreateTerminalParams": {
"type": "object",
"description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.",
@@ -8676,6 +8795,17 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
+ "CanvasExtensionSource": {
+ "enum": [
+ "user",
+ "project",
+ "session",
+ "plugin",
+ "unknown"
+ ],
+ "type": "string",
+ "description": "Where the runtime discovered an extension that declares a canvas type."
+ },
"TerminalClaim": {
"oneOf": [
{
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index ee4eb76af..5752d5bd9 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -1014,6 +1014,11 @@
"type": "object",
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
+ "canvases": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ },
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
"description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding\ncreation mode."
@@ -5153,6 +5158,53 @@
"title"
]
},
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "extensionSource": {
+ "$ref": "#/$defs/CanvasExtensionSource",
+ "description": "Where the runtime discovered the declaring extension."
+ },
+ "extensionName": {
+ "type": "string",
+ "description": "Human-readable extension name, when available."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Human-readable canvas type name."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of what the canvas type does."
+ },
+ "requiresInput": {
+ "type": "boolean",
+ "description": "Whether opening this canvas requires an input flow."
+ },
+ "actionCount": {
+ "type": "number",
+ "description": "Number of actions the canvas declares."
+ }
+ },
+ "required": [
+ "canvasId",
+ "extensionId",
+ "extensionSource",
+ "displayName",
+ "description",
+ "requiresInput",
+ "actionCount"
+ ]
+ },
"CanvasReference": {
"type": "object",
"description": "Stable reference to a subscribable canvas channel.\n\nChat state intentionally carries only this reference so the experimental\ncanvas presentation model can evolve without changing the stable chat\nchannel shape.",
@@ -7067,6 +7119,17 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
+ "CanvasExtensionSource": {
+ "enum": [
+ "user",
+ "project",
+ "session",
+ "plugin",
+ "unknown"
+ ],
+ "type": "string",
+ "description": "Where the runtime discovered an extension that declares a canvas type."
+ },
"TerminalClaim": {
"oneOf": [
{
diff --git a/schema/state.schema.json b/schema/state.schema.json
index ef58f86b0..d70638e5b 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -623,6 +623,11 @@
"type": "object",
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
+ "canvases": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ },
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
"description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding\ncreation mode."
@@ -4762,6 +4767,53 @@
"title"
]
},
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
+ },
+ "extensionId": {
+ "type": "string",
+ "description": "Stable identifier of the extension that declares the canvas."
+ },
+ "extensionSource": {
+ "$ref": "#/$defs/CanvasExtensionSource",
+ "description": "Where the runtime discovered the declaring extension."
+ },
+ "extensionName": {
+ "type": "string",
+ "description": "Human-readable extension name, when available."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Human-readable canvas type name."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of what the canvas type does."
+ },
+ "requiresInput": {
+ "type": "boolean",
+ "description": "Whether opening this canvas requires an input flow."
+ },
+ "actionCount": {
+ "type": "number",
+ "description": "Number of actions the canvas declares."
+ }
+ },
+ "required": [
+ "canvasId",
+ "extensionId",
+ "extensionSource",
+ "displayName",
+ "description",
+ "requiresInput",
+ "actionCount"
+ ]
+ },
"CanvasReference": {
"type": "object",
"description": "Stable reference to a subscribable canvas channel.\n\nChat state intentionally carries only this reference so the experimental\ncanvas presentation model can evolve without changing the stable chat\nchannel shape.",
@@ -6754,6 +6806,17 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
+ "CanvasExtensionSource": {
+ "enum": [
+ "user",
+ "project",
+ "session",
+ "plugin",
+ "unknown"
+ ],
+ "type": "string",
+ "description": "Where the runtime discovered an extension that declares a canvas type."
+ },
"ChangesetStatus": {
"enum": [
"computing",
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index 0f71d3ec0..1a86fed39 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -751,7 +751,7 @@ function generateDiscriminatedUnion(project: Project, cfg: UnionConfig): string
const STATE_ENUMS = [
'ChannelRecoveryKind',
'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'CanvasExtensionSource',
'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind',
'ChatInputQuestionKind', 'ChatInputResponseKind', 'SessionInputRequestKind',
'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus',
@@ -793,6 +793,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin
{ name: 'BackgroundShellWork' },
{ name: 'BackgroundSubagentWork' },
{ name: 'ChatState', mutable: true },
+ { name: 'CanvasTypeDeclaration' },
{ name: 'CanvasReference' },
{ name: 'CanvasState', mutable: true },
{ name: 'ChatInputOption' },
@@ -2333,6 +2334,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str
{ name: 'CreateChatParams' },
{ name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination' }, { name: 'ChatMoveToNewSessionDestination' }, { name: 'MoveChatParams' }, { name: 'MoveChatResult' },
+ { name: 'ListCanvasTypesParams' }, { name: 'ListCanvasTypesResult' }, { name: 'OpenCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index e0dfc2e17..d8418a33c 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -733,7 +733,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'CanvasExtensionSource',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -766,6 +766,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin
{ name: 'SessionChatSummary' },
{ name: 'ChangesSummary' },
{ name: 'ChatState' },
+ { name: 'CanvasTypeDeclaration' },
{ name: 'CanvasReference' },
{ name: 'CanvasState' },
{ name: 'ChatSummary' },
@@ -1817,6 +1818,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str
{ name: 'DisposeSessionParams' },
{ name: 'ForkChatSource' }, { name: 'SideChatSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination' }, { name: 'ChatMoveToNewSessionDestination' }, { name: 'MoveChatParams' }, { name: 'MoveChatResult' },
+ { name: 'ListCanvasTypesParams' }, { name: 'ListCanvasTypesResult' }, { name: 'OpenCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index 8cbffe506..f0e1b5c8c 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -996,7 +996,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'CanvasExtensionSource',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -1013,7 +1013,7 @@ const STATE_STRUCTS = [
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'BackgroundShellWork', 'BackgroundSubagentWork',
- 'CanvasReference', 'CanvasState',
+ 'CanvasTypeDeclaration', 'CanvasReference', 'CanvasState',
'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest',
'SessionToolAuthenticationRequest',
'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'ChatConfigState', 'Turn', 'ActiveTurn', 'Message',
@@ -1798,6 +1798,7 @@ const COMMAND_STRUCTS = [
'CreateSessionParams', 'DisposeSessionParams',
'CreateChatParams', 'DisposeChatParams',
'ChatMoveToSessionDestination', 'ChatMoveToNewSessionDestination', 'MoveChatParams', 'MoveChatResult',
+ 'ListCanvasTypesParams', 'ListCanvasTypesResult', 'OpenCanvasParams',
'ListSessionsParams', 'ListSessionsResult',
'ResourceReadParams', 'ResourceReadResult',
'ResourceWriteParams', 'ResourceWriteResult',
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index e5e6ec622..b08c46f4b 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -842,7 +842,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'CanvasExtensionSource',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -888,6 +888,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str
{ name: 'ConfigSchema' },
{ name: 'PendingMessage' },
{ name: 'ChatState' },
+ { name: 'CanvasTypeDeclaration' },
{ name: 'CanvasReference' },
{ name: 'CanvasState' },
{ name: 'ChatSummary' },
@@ -1864,6 +1865,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s
{ name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination', omitDiscriminants: true }, { name: 'ChatMoveToNewSessionDestination', omitDiscriminants: true },
{ name: 'MoveChatParams' }, { name: 'MoveChatResult' },
+ { name: 'ListCanvasTypesParams' }, { name: 'ListCanvasTypesResult' }, { name: 'OpenCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
@@ -1936,7 +1938,7 @@ function generateCommandsFile(project: Project): string {
lines.push('#[allow(unused_imports)]');
lines.push('use crate::actions::{ActionEnvelope, StateAction};');
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ChannelFlowControl, ChannelReceiveLimits, ContentRef, Message, MessageAttachment, ModelSelection, ResumedChannelSubscription, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TcpConnectionsCapability, TelemetryCapabilities, TerminalClaim, TextRange, Turn};');
+ lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, CanvasTypeDeclaration, ChannelFlowControl, ChannelReceiveLimits, ContentRef, Message, MessageAttachment, ModelSelection, ResumedChannelSubscription, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TcpConnectionsCapability, TelemetryCapabilities, TerminalClaim, TextRange, Turn};');
lines.push('');
lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n');
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index 69ed7ac63..a32edff88 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -689,7 +689,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'CanvasExtensionSource',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -751,7 +751,7 @@ const STATE_STRUCTS = [
'TerminalRunningLifecycleState', 'TerminalExitedLifecycleState', 'TerminalState',
'TerminalUnclassifiedPart', 'TerminalCommandPart',
'UsageInfo', 'ErrorInfo', 'Snapshot', 'EmptyState',
- 'CanvasReference', 'CanvasState',
+ 'CanvasTypeDeclaration', 'CanvasReference', 'CanvasState',
'Changeset', 'ChangesetCapabilities', 'ChangesetState', 'ChangesetFile', 'ChangesetOperation',
'AnnotationsSummary', 'AnnotationsState', 'AnnotationOrigin', 'Annotation', 'AnnotationEntry',
'TelemetryCapabilities',
@@ -1720,6 +1720,7 @@ const COMMAND_STRUCTS = [
'CreateSessionParams', 'DisposeSessionParams',
'CreateChatParams', 'DisposeChatParams',
'ChatMoveToSessionDestination', 'ChatMoveToNewSessionDestination', 'MoveChatParams', 'MoveChatResult',
+ 'ListCanvasTypesParams', 'ListCanvasTypesResult', 'OpenCanvasParams',
'ListSessionsParams', 'ListSessionsResult',
'ResourceReadParams', 'ResourceReadResult',
'ResourceWriteParams', 'ResourceWriteResult',
diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts
new file mode 100644
index 000000000..a752809b7
--- /dev/null
+++ b/types/channels-canvas/commands.ts
@@ -0,0 +1,74 @@
+/**
+ * Canvas Commands — discovery and explicit opening for one exact chat.
+ *
+ * @module channels-canvas/commands
+ */
+
+import type { BaseParams } from '../common/commands.js';
+import type { URI } from '../common/state.js';
+import type { AgentCapabilities } from '../channels-root/state.js';
+import type { ChatState } from '../channels-chat/state.js';
+import type { CanvasTypeDeclaration } from './state.js';
+
+// ─── listCanvasTypes ─────────────────────────────────────────────────────────
+
+/**
+ * Lists canvas types currently available to open for one exact chat.
+ *
+ * This read MAY lazily resume an existing persisted chat so its extension
+ * registry can be reconstructed. It MUST NOT create a backing runtime for a
+ * brand-new chat, send a user message, or invoke a model.
+ *
+ * Clients MUST NOT call this command unless the selected agent advertises
+ * {@link AgentCapabilities.canvases}.
+ *
+ * @category Commands
+ * @method listCanvasTypes
+ * @direction Client → Server
+ * @messageType Request
+ * @version 1
+ */
+export interface ListCanvasTypesParams extends BaseParams {
+ /** Exact chat URI whose extension registry should be queried. */
+ channel: URI;
+}
+
+/**
+ * Canvas types available to open for the requested chat.
+ *
+ * @category Commands
+ */
+export interface ListCanvasTypesResult {
+ /** Available canvas types and their declaring extensions. */
+ items: CanvasTypeDeclaration[];
+}
+
+// ─── openCanvas ──────────────────────────────────────────────────────────────
+
+/**
+ * Opens or focuses one stable canvas instance in an exact chat.
+ *
+ * The resulting live instance is published through the chat's
+ * {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
+ * Repeating this command with the same `instanceId` focuses or refreshes that
+ * logical instance rather than creating a duplicate.
+ *
+ * Clients MUST NOT call this command unless the selected agent advertises
+ * {@link AgentCapabilities.canvases}.
+ *
+ * @category Commands
+ * @method openCanvas
+ * @direction Client → Server
+ * @messageType Request
+ * @version 1
+ */
+export interface OpenCanvasParams extends BaseParams {
+ /** Exact chat URI that owns the canvas instance. */
+ channel: URI;
+ /** Stable identifier of the extension that declares the canvas. */
+ extensionId: string;
+ /** Provider-local canvas type identifier. */
+ canvasId: string;
+ /** Caller-chosen stable instance identifier. */
+ instanceId: string;
+}
diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts
index 64f5396f2..21d0ba5fd 100644
--- a/types/channels-canvas/state.ts
+++ b/types/channels-canvas/state.ts
@@ -9,6 +9,53 @@
import type { URI } from '../common/state.js';
+/**
+ * Where the runtime discovered an extension that declares a canvas type.
+ *
+ * @category Canvas State
+ * @nonexhaustive
+ */
+export const enum CanvasExtensionSource {
+ /** Extension installed for the current user. */
+ User = 'user',
+ /** Extension discovered in the current project. */
+ Project = 'project',
+ /** Extension installed only for the current session. */
+ Session = 'session',
+ /** Extension contributed by an installed plugin. */
+ Plugin = 'plugin',
+ /** Extension source is unavailable or not recognized by this protocol version. */
+ Unspecified = 'unknown',
+}
+
+/**
+ * One canvas type currently available to open for an exact chat.
+ *
+ * This is discovery metadata, not a live canvas instance. Live instances are
+ * represented by {@link CanvasReference} entries on chat state and their
+ * corresponding {@link CanvasState} channels.
+ *
+ * @category Canvas State
+ */
+export interface CanvasTypeDeclaration {
+ /** Provider-local canvas type identifier. */
+ canvasId: string;
+ /** Stable identifier of the extension that declares the canvas. */
+ extensionId: string;
+ /** Where the runtime discovered the declaring extension. */
+ extensionSource: CanvasExtensionSource;
+ /** Human-readable extension name, when available. */
+ extensionName?: string;
+ /** Human-readable canvas type name. */
+ displayName: string;
+ /** Description of what the canvas type does. */
+ description: string;
+ /** Whether opening this canvas requires an input flow. */
+ requiresInput: boolean;
+ /** Number of actions the canvas declares. */
+ actionCount: number;
+}
+
/**
* Stable reference to a subscribable canvas channel.
*
diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts
index c1447309d..3184ea09e 100644
--- a/types/channels-root/state.ts
+++ b/types/channels-root/state.ts
@@ -106,6 +106,12 @@ export interface AgentInfo {
* @category Root State
*/
export interface AgentCapabilities {
+ /**
+ * The agent can list canvas types for an exact chat with `listCanvasTypes`
+ * and open them with `openCanvas`. Live instances are published through the
+ * chat's canvas references and their `ahp-canvas:` state channels.
+ */
+ canvases?: Record;
/**
* The agent can host more than one concurrent chat per session. When absent,
* clients MUST NOT call `createChat` to open chats beyond the default one the
diff --git a/types/commands.ts b/types/commands.ts
index 3bfd44073..763ccbfdb 100644
--- a/types/commands.ts
+++ b/types/commands.ts
@@ -11,6 +11,7 @@ export * from './common/commands.js';
export * from './channels-root/commands.js';
export * from './channels-session/commands.js';
export * from './channels-chat/commands.js';
+export * from './channels-canvas/commands.js';
export * from './channels-terminal/commands.js';
export * from './channels-changeset/commands.js';
export * from './channels-resource-watch/commands.js';
diff --git a/types/common/messages.ts b/types/common/messages.ts
index 010a266bc..c3fea651e 100644
--- a/types/common/messages.ts
+++ b/types/common/messages.ts
@@ -60,6 +60,11 @@ import type {
MoveChatResult,
DisposeChatParams,
} from '../channels-chat/commands.js';
+import type {
+ ListCanvasTypesParams,
+ ListCanvasTypesResult,
+ OpenCanvasParams,
+} from '../channels-canvas/commands.js';
import type {
CreateTerminalParams,
DisposeTerminalParams,
@@ -174,6 +179,8 @@ export interface CommandMap {
'createTcpConnection': { params: CreateTcpConnectionParams; result: null };
'moveChat': { params: MoveChatParams; result: MoveChatResult };
'disposeChat': { params: DisposeChatParams; result: null };
+ 'listCanvasTypes': { params: ListCanvasTypesParams; result: ListCanvasTypesResult };
+ 'openCanvas': { params: OpenCanvasParams; result: null };
'createTerminal': { params: CreateTerminalParams; result: null };
'disposeTerminal': { params: DisposeTerminalParams; result: null };
'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult };
diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts
index 5e72d540e..dea768c5f 100644
--- a/types/version/message-checks.ts
+++ b/types/version/message-checks.ts
@@ -60,6 +60,8 @@ type _ExpectedCommands =
| 'createTcpConnection'
| 'moveChat'
| 'disposeChat'
+ | 'listCanvasTypes'
+ | 'openCanvas'
| 'createTerminal'
| 'disposeTerminal'
| 'createResourceWatch'
From ee37db3caa4af96ada30868800637d5d6a42d5e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Wed, 14 Oct 2026 14:54:51 -0700
Subject: [PATCH 02/12] canvas: mark discovery API experimental
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
---
.../Generated/Commands.generated.cs | 4 ++++
.../Generated/State.generated.cs | 14 +++++++++++---
clients/go/ahptypes/commands.generated.go | 4 ++++
clients/go/ahptypes/state.generated.go | 8 ++++++++
.../agenthostprotocol/generated/State.generated.kt | 5 +++++
.../Generated/State.generated.swift | 5 +++++
.../20261012-canvas-discovery-commands.json | 2 +-
schema/actions.schema.json | 4 ++--
schema/commands.schema.json | 6 +++---
schema/errors.schema.json | 6 +++---
schema/notifications.schema.json | 4 ++--
schema/state.schema.json | 4 ++--
types/channels-canvas/commands.ts | 6 ++++++
types/channels-canvas/state.ts | 3 +++
types/channels-root/state.ts | 5 +++++
15 files changed, 64 insertions(+), 16 deletions(-)
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index a6acf50fb..e042afc3b 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1181,6 +1181,10 @@ public sealed record ListCanvasTypesResult
/// Repeating this command with the same `instanceId` focuses or refreshes that
/// logical instance rather than creating a duplicate.
///
+/// The null result is not presentation state. Clients MUST observe
+/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
+/// authoritative membership, availability, and presentation state.
+///
/// Clients MUST NOT call this command unless the selected agent advertises
/// {@link AgentCapabilities.canvases}.
public sealed record OpenCanvasParams
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 525ff95d0..6544bd358 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -2115,9 +2115,14 @@ public sealed record AgentInfo
/// per-capability options.
public sealed record AgentCapabilities
{
- /// The agent can list canvas types for an exact chat with `listCanvasTypes`
+ /// Experimental Canvas API.
+ ///
+ /// The agent can list canvas types for an exact chat with `listCanvasTypes`
/// and open them with `openCanvas`. Live instances are published through the
- /// chat's canvas references and their `ahp-canvas:` state channels.
+ /// chat's canvas references and their `ahp-canvas:` state channels.
+ ///
+ /// Clients MUST treat absence as unsupported and SHOULD tolerate additive
+ /// discovery metadata while the Canvas channel remains experimental.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Canvases { get; init; }
@@ -2678,7 +2683,10 @@ public sealed class ChatState
///
/// This is discovery metadata, not a live canvas instance. Live instances are
/// represented by {@link CanvasReference} entries on chat state and their
-/// corresponding {@link CanvasState} channels.
+/// corresponding {@link CanvasState} channels.
+///
+/// This experimental object may gain optional discovery metadata. Clients
+/// SHOULD ignore fields they do not understand.
public sealed record CanvasTypeDeclaration
{
/// Provider-local canvas type identifier.
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index ecf4a6743..b8ddde896 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -793,6 +793,10 @@ type ListCanvasTypesResult struct {
// Repeating this command with the same `instanceId` focuses or refreshes that
// logical instance rather than creating a duplicate.
//
+// The null result is not presentation state. Clients MUST observe
+// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
+// authoritative membership, availability, and presentation state.
+//
// Clients MUST NOT call this command unless the selected agent advertises
// {@link AgentCapabilities.canvases}.
type OpenCanvasParams struct {
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index a1526c0ff..0d5936327 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -802,9 +802,14 @@ type AgentInfo struct {
// corresponding client commands MUST NOT be used. Sub-fields carry
// per-capability options.
type AgentCapabilities struct {
+ // Experimental Canvas API.
+ //
// The agent can list canvas types for an exact chat with `listCanvasTypes`
// and open them with `openCanvas`. Live instances are published through the
// chat's canvas references and their `ahp-canvas:` state channels.
+ //
+ // Clients MUST treat absence as unsupported and SHOULD tolerate additive
+ // discovery metadata while the Canvas channel remains experimental.
Canvases map[string]json.RawMessage `json:"canvases,omitempty"`
// The agent can host more than one concurrent chat per session. When absent,
// clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -1505,6 +1510,9 @@ type ChatState struct {
// This is discovery metadata, not a live canvas instance. Live instances are
// represented by {@link CanvasReference} entries on chat state and their
// corresponding {@link CanvasState} channels.
+//
+// This experimental object may gain optional discovery metadata. Clients
+// SHOULD ignore fields they do not understand.
type CanvasTypeDeclaration struct {
// Provider-local canvas type identifier.
CanvasId string `json:"canvasId"`
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 607c36eb0..6e9890d00 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1548,9 +1548,14 @@ data class AgentInfo(
@Serializable
data class AgentCapabilities(
/**
+ * Experimental Canvas API.
+ *
* The agent can list canvas types for an exact chat with `listCanvasTypes`
* and open them with `openCanvas`. Live instances are published through the
* chat's canvas references and their `ahp-canvas:` state channels.
+ *
+ * Clients MUST treat absence as unsupported and SHOULD tolerate additive
+ * discovery metadata while the Canvas channel remains experimental.
*/
val canvases: Map? = null,
/**
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index 9146fcde7..f65346bd5 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1520,9 +1520,14 @@ public struct AgentInfo: Codable, Sendable {
}
public struct AgentCapabilities: Codable, Sendable {
+ /// Experimental Canvas API.
+ ///
/// The agent can list canvas types for an exact chat with `listCanvasTypes`
/// and open them with `openCanvas`. Live instances are published through the
/// chat's canvas references and their `ahp-canvas:` state channels.
+ ///
+ /// Clients MUST treat absence as unsupported and SHOULD tolerate additive
+ /// discovery metadata while the Canvas channel remains experimental.
public var canvases: [String: AnyCodable]?
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
diff --git a/docs/.changes/20261012-canvas-discovery-commands.json b/docs/.changes/20261012-canvas-discovery-commands.json
index 7e113fe82..a4ea67f7b 100644
--- a/docs/.changes/20261012-canvas-discovery-commands.json
+++ b/docs/.changes/20261012-canvas-discovery-commands.json
@@ -1,4 +1,4 @@
{
"type": "added",
- "message": "Capability-gated `listCanvasTypes` and `openCanvas` commands for discovering extension canvases and opening them in an exact chat."
+ "message": "Experimental, capability-gated `listCanvasTypes` and `openCanvas` commands for discovering extension canvases and opening them in an exact chat."
}
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index 7ba69625c..e6ddf357f 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -3341,7 +3341,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -7484,7 +7484,7 @@
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index e2cbbc0ed..a8303ee71 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -1612,7 +1612,7 @@
},
"OpenCanvasParams": {
"type": "object",
- "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -2708,7 +2708,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -6851,7 +6851,7 @@
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 846ddfc1e..564e9725c 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -715,7 +715,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -4858,7 +4858,7 @@
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
@@ -7804,7 +7804,7 @@
},
"OpenCanvasParams": {
"type": "object",
- "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index 5752d5bd9..4a590568b 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -1017,7 +1017,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -5160,7 +5160,7 @@
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
diff --git a/schema/state.schema.json b/schema/state.schema.json
index d70638e5b..36d3c80a9 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -626,7 +626,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "The agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels."
+ "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -4769,7 +4769,7 @@
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.",
+ "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts
index a752809b7..5d3337d66 100644
--- a/types/channels-canvas/commands.ts
+++ b/types/channels-canvas/commands.ts
@@ -1,6 +1,8 @@
/**
* Canvas Commands — discovery and explicit opening for one exact chat.
*
+ * Stability: 1 - Experimental
+ *
* @module channels-canvas/commands
*/
@@ -53,6 +55,10 @@ export interface ListCanvasTypesResult {
* Repeating this command with the same `instanceId` focuses or refreshes that
* logical instance rather than creating a duplicate.
*
+ * The null result is not presentation state. Clients MUST observe
+ * {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
+ * authoritative membership, availability, and presentation state.
+ *
* Clients MUST NOT call this command unless the selected agent advertises
* {@link AgentCapabilities.canvases}.
*
diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts
index 21d0ba5fd..b8dbf9057 100644
--- a/types/channels-canvas/state.ts
+++ b/types/channels-canvas/state.ts
@@ -35,6 +35,9 @@ export const enum CanvasExtensionSource {
* represented by {@link CanvasReference} entries on chat state and their
* corresponding {@link CanvasState} channels.
*
+ * This experimental object may gain optional discovery metadata. Clients
+ * SHOULD ignore fields they do not understand.
+ *
* @category Canvas State
*/
export interface CanvasTypeDeclaration {
diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts
index 3184ea09e..85fe64f6d 100644
--- a/types/channels-root/state.ts
+++ b/types/channels-root/state.ts
@@ -107,9 +107,14 @@ export interface AgentInfo {
*/
export interface AgentCapabilities {
/**
+ * Experimental Canvas API.
+ *
* The agent can list canvas types for an exact chat with `listCanvasTypes`
* and open them with `openCanvas`. Live instances are published through the
* chat's canvas references and their `ahp-canvas:` state channels.
+ *
+ * Clients MUST treat absence as unsupported and SHOULD tolerate additive
+ * discovery metadata while the Canvas channel remains experimental.
*/
canvases?: Record;
/**
From a9a99560cd73eafc9509e2a34b3b0751906415bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Wed, 14 Oct 2026 14:56:58 -0700
Subject: [PATCH 03/12] canvas: refresh generated Rust docs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
---
clients/rust/crates/ahp-types/src/commands.rs | 4 ++++
clients/rust/crates/ahp-types/src/state.rs | 8 ++++++++
2 files changed, 12 insertions(+)
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 1d5d092e8..89efc0af7 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -1090,6 +1090,10 @@ pub struct ListCanvasTypesResult {
/// Repeating this command with the same `instanceId` focuses or refreshes that
/// logical instance rather than creating a duplicate.
///
+/// The null result is not presentation state. Clients MUST observe
+/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
+/// authoritative membership, availability, and presentation state.
+///
/// Clients MUST NOT call this command unless the selected agent advertises
/// {@link AgentCapabilities.canvases}.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index caf399802..4999c2377 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1823,9 +1823,14 @@ pub struct AgentInfo {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
+ /// Experimental Canvas API.
+ ///
/// The agent can list canvas types for an exact chat with `listCanvasTypes`
/// and open them with `openCanvas`. Live instances are published through the
/// chat's canvas references and their `ahp-canvas:` state channels.
+ ///
+ /// Clients MUST treat absence as unsupported and SHOULD tolerate additive
+ /// discovery metadata while the Canvas channel remains experimental.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canvases: Option,
/// The agent can host more than one concurrent chat per session. When absent,
@@ -2218,6 +2223,9 @@ pub struct ChatState {
/// This is discovery metadata, not a live canvas instance. Live instances are
/// represented by {@link CanvasReference} entries on chat state and their
/// corresponding {@link CanvasState} channels.
+///
+/// This experimental object may gain optional discovery metadata. Clients
+/// SHOULD ignore fields they do not understand.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasTypeDeclaration {
From 96213748000c55efc0f495cd6628c307692b6b90 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Wed, 14 Oct 2026 23:56:42 -0700
Subject: [PATCH 04/12] Model Canvas discovery as chat state
Replace snapshot-only Canvas listing with an authoritative exact-chat registry, explicit cold materialization action, and URI-addressed Canvas opening. Normalize provider metadata and update every generated client, reducer, fixture, and specification.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
---
.../Generated/Actions.generated.cs | 37 +++++
.../Generated/Commands.generated.cs | 43 ++---
.../JsonSerializerContext.generated.cs | 6 +-
.../Generated/State.generated.cs | 75 ++++++---
.../Generated/ActionMetadata.generated.cs | 6 +
.../dotnet/src/AgentHostProtocol/Reducers.cs | 5 +
.../TypesRoundTripFixtures.cs | 2 +
clients/go/ahp/reducers.go | 5 +
clients/go/ahptypes/actions.generated.go | 43 +++++
clients/go/ahptypes/commands.generated.go | 33 +---
clients/go/ahptypes/roundtrip_fixture_test.go | 2 +
clients/go/ahptypes/state.generated.go | 64 ++++++--
.../microsoft/agenthostprotocol/Reducers.kt | 6 +
.../generated/Actions.generated.kt | 22 +++
.../generated/Commands.generated.kt | 32 +---
.../generated/State.generated.kt | 64 ++++++--
.../agenthostprotocol/RoundTripCorpusTest.kt | 2 +
clients/rust/crates/ahp-types/src/actions.rs | 47 +++++-
clients/rust/crates/ahp-types/src/commands.rs | 47 ++----
clients/rust/crates/ahp-types/src/state.rs | 71 ++++++--
.../ahp-types/tests/roundtrip_corpus.rs | 3 +-
clients/rust/crates/ahp/src/reducers.rs | 6 +
.../Generated/Actions.generated.swift | 38 +++++
.../Generated/Commands.generated.swift | 44 +----
.../Generated/State.generated.swift | 83 +++++++---
.../Sources/AgentHostProtocol/Reducers.swift | 8 +
.../TypesRoundTripFixtureTests.swift | 2 +
.../typescript/test/types-round-trip.test.ts | 2 +
.../20261012-canvas-discovery-commands.json | 2 +-
docs/specification/chat-channel.md | 38 +++++
schema/actions.schema.json | 103 +++++++++---
schema/commands.schema.json | 151 +++++++++++-------
schema/errors.schema.json | 151 +++++++++++-------
schema/notifications.schema.json | 69 +++++---
schema/state.schema.json | 69 +++++---
scripts/generate-csharp.ts | 6 +-
scripts/generate-go.ts | 6 +-
scripts/generate-kotlin.ts | 6 +-
scripts/generate-rust.ts | 10 +-
scripts/generate-swift.ts | 6 +-
types/action-origin.generated.ts | 8 +
types/channels-canvas/commands.ts | 52 ++----
types/channels-canvas/state.ts | 58 +++++--
types/channels-chat/actions.ts | 40 ++++-
types/channels-chat/reducer.ts | 10 ++
types/channels-chat/state.ts | 13 +-
types/channels-root/state.ts | 11 +-
types/common/actions.ts | 6 +
types/common/messages.ts | 7 +-
...293-chat-canvasregistryrequested-noop.json | 23 +++
...t-canvasregistrychanged-sets-registry.json | 57 +++++++
...canvasregistrychanged-clears-registry.json | 26 +++
.../round-trips/076-chat-canvas-registry.json | 76 +++++++++
...chat-canvas-registry-requested-action.json | 14 ++
...8-chat-canvas-registry-changed-action.json | 20 +++
...open-canvas-client-addressed-resource.json | 20 +++
types/version/message-checks.ts | 1 -
types/version/registry.test.ts | 17 ++
types/version/registry.ts | 2 +
59 files changed, 1372 insertions(+), 504 deletions(-)
create mode 100644 types/test-cases/reducers/293-chat-canvasregistryrequested-noop.json
create mode 100644 types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json
create mode 100644 types/test-cases/reducers/295-chat-canvasregistrychanged-clears-registry.json
create mode 100644 types/test-cases/round-trips/076-chat-canvas-registry.json
create mode 100644 types/test-cases/round-trips/077-chat-canvas-registry-requested-action.json
create mode 100644 types/test-cases/round-trips/078-chat-canvas-registry-changed-action.json
create mode 100644 types/test-cases/round-trips/079-open-canvas-client-addressed-resource.json
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
index 0f5cbd13d..37ebeac23 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
@@ -87,6 +87,10 @@ public ActionType(string value)
public static readonly ActionType ChatCanvasesChanged = new ActionType("chat/canvasesChanged");
+ public static readonly ActionType ChatCanvasRegistryRequested = new ActionType("chat/canvasRegistryRequested");
+
+ public static readonly ActionType ChatCanvasRegistryChanged = new ActionType("chat/canvasRegistryChanged");
+
public static readonly ActionType CanvasStateChanged = new ActionType("canvas/stateChanged");
public static readonly ActionType ChatWorkingDirectorySet = new ActionType("chat/workingDirectorySet");
@@ -1847,6 +1851,37 @@ public sealed record ChatCanvasesChangedAction
public List? Canvases { get; init; }
}
+/// Requests materialization of the exact chat's Canvas type registry.
+///
+/// This operation is potentially effectful: for a persisted cold chat, the host
+/// may resume its backing runtime, discover extensions, and execute extension
+/// startup code. It MUST NOT create a backing for a brand-new chat, send a user
+/// message, or invoke a model.
+///
+/// The reducer is a no-op. The host remains authoritative and publishes the
+/// complete result through
+/// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
+/// Clients do not need to dispatch this action when
+/// {@link ChatState.canvasRegistry} is already present because live provider
+/// registration changes are published reactively.
+public sealed record ChatCanvasRegistryRequestedAction
+{
+ public ActionType Type { get; init; } = ActionType.ChatCanvasRegistryRequested;
+}
+
+/// The Canvas type registry for this exact chat changed.
+///
+/// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
+/// Set to `undefined` when the registry is no longer materialized or available.
+public sealed record ChatCanvasRegistryChangedAction
+{
+ public ActionType Type { get; init; } = ActionType.ChatCanvasRegistryChanged;
+
+ /// Authoritative registry, or `undefined` to clear it.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public CanvasRegistry? CanvasRegistry { get; init; }
+}
+
/// The presentation state for this canvas changed.
///
/// Replaces the subscribed canvas channel state entirely. Full-replacement
@@ -2922,6 +2957,8 @@ public StateActionConverter()
["chat/movableChanged"] = typeof(ChatMovableChangedAction),
["chat/changesetsChanged"] = typeof(ChatChangesetsChangedAction),
["chat/canvasesChanged"] = typeof(ChatCanvasesChangedAction),
+ ["chat/canvasRegistryRequested"] = typeof(ChatCanvasRegistryRequestedAction),
+ ["chat/canvasRegistryChanged"] = typeof(ChatCanvasRegistryChangedAction),
["canvas/stateChanged"] = typeof(CanvasStateChangedAction),
["chat/workingDirectorySet"] = typeof(ChatWorkingDirectorySetAction),
["chat/workingDirectoryRemoved"] = typeof(ChatWorkingDirectoryRemovedAction),
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index e042afc3b..6f2c0dd96 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1147,39 +1147,13 @@ public sealed record MoveChatResult
public required string Session { get; init; }
}
-/// Lists canvas types currently available to open for one exact chat.
-///
-/// This read MAY lazily resume an existing persisted chat so its extension
-/// registry can be reconstructed. It MUST NOT create a backing runtime for a
-/// brand-new chat, send a user message, or invoke a model.
-///
-/// Clients MUST NOT call this command unless the selected agent advertises
-/// {@link AgentCapabilities.canvases}.
-public sealed record ListCanvasTypesParams
-{
- /// Exact chat URI whose extension registry should be queried.
- public required string Channel { get; init; }
-
- /// Optional JSON-serializable metadata associated with this request.
- /// Receivers MUST ignore keys they do not understand.
- [JsonPropertyName("_meta")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public Dictionary? Meta { get; init; }
-}
-
-/// Canvas types available to open for the requested chat.
-public sealed record ListCanvasTypesResult
-{
- /// Available canvas types and their declaring extensions.
- public required List Items { get; init; }
-}
-
-/// Opens or focuses one stable canvas instance in an exact chat.
+/// Opens or focuses one client-addressed canvas resource in an exact chat.
///
/// The resulting live instance is published through the chat's
/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-/// Repeating this command with the same `instanceId` focuses or refreshes that
-/// logical instance rather than creating a duplicate.
+/// Repeating this command with the same `channel` focuses or refreshes that
+/// logical instance rather than creating a duplicate. Reusing the channel for
+/// a different chat, extension, or canvas type MUST fail with `Conflict`.
///
/// The null result is not presentation state. Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -1189,7 +1163,8 @@ public sealed record ListCanvasTypesResult
/// {@link AgentCapabilities.canvases}.
public sealed record OpenCanvasParams
{
- /// Exact chat URI that owns the canvas instance.
+ /// Client-chosen URI for the canvas resource, for example
+ /// `ahp-canvas:/<uuid>`.
public required string Channel { get; init; }
/// Optional JSON-serializable metadata associated with this request.
@@ -1198,14 +1173,14 @@ public sealed record OpenCanvasParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Meta { get; init; }
+ /// Exact chat URI that owns the canvas instance.
+ public required string Chat { get; init; }
+
/// Stable identifier of the extension that declares the canvas.
public required string ExtensionId { get; init; }
/// Provider-local canvas type identifier.
public required string CanvasId { get; init; }
-
- /// Caller-chosen stable instance identifier.
- public required string InstanceId { get; init; }
}
/// Returns a list of session summaries. Used to populate session lists and sidebars.
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index 898b036ba..43db555f5 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -83,8 +83,10 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(BackgroundSubagentWork))]
[JsonSerializable(typeof(BackgroundWork))]
[JsonSerializable(typeof(BackgroundWorkKind))]
+[JsonSerializable(typeof(CanvasExtensionDeclaration))]
[JsonSerializable(typeof(CanvasExtensionSource))]
[JsonSerializable(typeof(CanvasReference))]
+[JsonSerializable(typeof(CanvasRegistry))]
[JsonSerializable(typeof(CanvasState))]
[JsonSerializable(typeof(CanvasStateChangedAction))]
[JsonSerializable(typeof(CanvasTypeDeclaration))]
@@ -122,6 +124,8 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(ChatBackgroundWorkRemovedAction))]
[JsonSerializable(typeof(ChatBackgroundWorkSetAction))]
[JsonSerializable(typeof(ChatCanvasesChangedAction))]
+[JsonSerializable(typeof(ChatCanvasRegistryChangedAction))]
+[JsonSerializable(typeof(ChatCanvasRegistryRequestedAction))]
[JsonSerializable(typeof(ChatChangesetsChangedAction))]
[JsonSerializable(typeof(ChatConfigChangedAction))]
[JsonSerializable(typeof(ChatConfigState))]
@@ -261,8 +265,6 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(JsonRpcSuccessResponse))]
[JsonSerializable(typeof(ListAutomationTriggerDefinitionsParams))]
[JsonSerializable(typeof(ListAutomationTriggerDefinitionsResult))]
-[JsonSerializable(typeof(ListCanvasTypesParams))]
-[JsonSerializable(typeof(ListCanvasTypesResult))]
[JsonSerializable(typeof(ListSessionsParams))]
[JsonSerializable(typeof(ListSessionsResult))]
[JsonSerializable(typeof(MarkdownResponsePart))]
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 6544bd358..f85143041 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -2117,12 +2117,15 @@ public sealed record AgentCapabilities
{
/// Experimental Canvas API.
///
- /// The agent can list canvas types for an exact chat with `listCanvasTypes`
- /// and open them with `openCanvas`. Live instances are published through the
- /// chat's canvas references and their `ahp-canvas:` state channels.
+ /// The agent can materialize an exact chat's
+ /// {@link ChatState.canvasRegistry | Canvas registry} through
+ /// `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
+ /// Live instances are published through the chat's canvas references and
+ /// their `ahp-canvas:` state channels.
///
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
- /// discovery metadata while the Canvas channel remains experimental.
+ /// registry metadata while the Canvas channel remains experimental. This
+ /// capability does not gate server-published Canvas references.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Canvases { get; init; }
@@ -2618,6 +2621,17 @@ public sealed class ChatState
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? BackgroundWork { get; set; }
+ /// Canvas types currently registered for this exact chat.
+ ///
+ /// Absence means the registry has not been materialized or is not currently
+ /// available. Clients that need it MAY dispatch
+ /// `chat/canvasRegistryRequested`; the host publishes the authoritative
+ /// registry through `chat/canvasRegistryChanged`. An empty
+ /// {@link CanvasRegistry.extensions} array is a loaded registry with no
+ /// available canvas types.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public CanvasRegistry? CanvasRegistry { get; set; }
+
/// Live canvases currently exposed by this chat.
///
/// Entries intentionally contain only subscribable channel references.
@@ -2679,7 +2693,29 @@ public sealed class ChatState
public Dictionary? Meta { get; set; }
}
-/// One canvas type currently available to open for an exact chat.
+/// One extension/provider that declares canvas types for an exact chat.
+///
+/// The extension metadata is normalized here instead of repeated on every
+/// {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
+/// infer its source or management behavior by parsing it.
+public sealed record CanvasExtensionDeclaration
+{
+ /// Stable opaque identifier of the extension/provider.
+ public required string ExtensionId { get; init; }
+
+ /// Where the runtime discovered the extension/provider.
+ public CanvasExtensionSource ExtensionSource { get; init; }
+
+ /// Human-readable extension/provider name, when available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ExtensionName { get; init; }
+
+ /// Canvas types declared by this extension/provider. `canvasId` MUST be
+ /// unique within this array.
+ public required List Canvases { get; init; }
+}
+
+/// One canvas type currently available to open.
///
/// This is discovery metadata, not a live canvas instance. Live instances are
/// represented by {@link CanvasReference} entries on chat state and their
@@ -2692,16 +2728,6 @@ public sealed record CanvasTypeDeclaration
/// Provider-local canvas type identifier.
public required string CanvasId { get; init; }
- /// Stable identifier of the extension that declares the canvas.
- public required string ExtensionId { get; init; }
-
- /// Where the runtime discovered the declaring extension.
- public CanvasExtensionSource ExtensionSource { get; init; }
-
- /// Human-readable extension name, when available.
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public string? ExtensionName { get; init; }
-
/// Human-readable canvas type name.
public required string DisplayName { get; init; }
@@ -2710,9 +2736,18 @@ public sealed record CanvasTypeDeclaration
/// Whether opening this canvas requires an input flow.
public bool RequiresInput { get; init; }
+}
- /// Number of actions the canvas declares.
- public long ActionCount { get; init; }
+/// Canvas types currently registered for one exact chat.
+///
+/// This is a compact presentation registry, not extension installation state
+/// and not the list of live instances. The host replaces the complete registry
+/// through `chat/canvasRegistryChanged` whenever provider registration changes.
+public sealed record CanvasRegistry
+{
+ /// Declaring extensions/providers. `extensionId` MUST be unique within this
+ /// array. An empty array is an authoritative loaded registry with no types.
+ public required List Extensions { get; init; }
}
/// Stable reference to a subscribable canvas channel.
@@ -2733,7 +2768,11 @@ public sealed record CanvasReference
/// redundantly carry its channel URI.
public sealed class CanvasState
{
- /// Stable caller-supplied instance identifier.
+ /// Provider-facing instance identifier.
+ ///
+ /// AHP clients address this instance by the subscribed `ahp-canvas:` channel
+ /// URI. They MUST NOT treat this provider identifier as a second resource
+ /// identity.
public required string InstanceId { get; set; }
/// Owning extension/provider identifier.
diff --git a/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs b/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs
index 1cc062b11..d7946852a 100644
--- a/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs
@@ -94,6 +94,12 @@ public static bool TryGetActionType(object action, out ActionType actionType)
case ChatCanvasesChangedAction value:
actionType = value.Type;
return true;
+ case ChatCanvasRegistryChangedAction value:
+ actionType = value.Type;
+ return true;
+ case ChatCanvasRegistryRequestedAction value:
+ actionType = value.Type;
+ return true;
case ChatChangesetsChangedAction value:
actionType = value.Type;
return true;
diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs
index 1f5ec3740..a0eae101f 100644
--- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs
+++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs
@@ -1054,6 +1054,11 @@ public static ReduceOutcome ApplyToChat(ChatState state, StateAction action)
case ChatCanvasesChangedAction a:
state.Canvases = CopyList(a.Canvases);
return ReduceOutcome.Applied;
+ case ChatCanvasRegistryRequestedAction:
+ return ReduceOutcome.NoOp;
+ case ChatCanvasRegistryChangedAction a:
+ state.CanvasRegistry = a.CanvasRegistry;
+ return ReduceOutcome.Applied;
case ChatWorkingDirectorySetAction a:
{
// Membership keyed by the directory URI, over this chat's subset of
diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
index ad368af9e..b9431b84f 100644
--- a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
+++ b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
@@ -192,6 +192,8 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type,
return Wrap(Ser.Deserialize(inputJson));
case "ResourceReadResult":
return Wrap(Ser.Deserialize(inputJson));
+ case "OpenCanvasParams":
+ return Wrap(Ser.Deserialize(inputJson));
default:
throw new Xunit.Sdk.XunitException(
$"round-trip fixture: unknown wire type \"{type}\". "
diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go
index 22fe67c01..f52bb86b7 100644
--- a/clients/go/ahp/reducers.go
+++ b/clients/go/ahp/reducers.go
@@ -647,6 +647,11 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R
state.Canvases = append([]ahptypes.CanvasReference(nil), a.Canvases...)
}
return ReduceOutcomeApplied
+ case *ahptypes.ChatCanvasRegistryRequestedAction:
+ return ReduceOutcomeNoOp
+ case *ahptypes.ChatCanvasRegistryChangedAction:
+ state.CanvasRegistry = a.CanvasRegistry
+ return ReduceOutcomeApplied
case *ahptypes.ChatWorkingDirectorySetAction:
for _, d := range state.WorkingDirectories {
if d == a.Directory {
diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go
index b66c0dab7..255b7a08d 100644
--- a/clients/go/ahptypes/actions.generated.go
+++ b/clients/go/ahptypes/actions.generated.go
@@ -50,6 +50,8 @@ const (
ActionTypeChatMovableChanged ActionType = "chat/movableChanged"
ActionTypeChatChangesetsChanged ActionType = "chat/changesetsChanged"
ActionTypeChatCanvasesChanged ActionType = "chat/canvasesChanged"
+ ActionTypeChatCanvasRegistryRequested ActionType = "chat/canvasRegistryRequested"
+ ActionTypeChatCanvasRegistryChanged ActionType = "chat/canvasRegistryChanged"
ActionTypeCanvasStateChanged ActionType = "canvas/stateChanged"
ActionTypeChatWorkingDirectorySet ActionType = "chat/workingDirectorySet"
ActionTypeChatWorkingDirectoryRemoved ActionType = "chat/workingDirectoryRemoved"
@@ -721,6 +723,33 @@ type ChatCanvasesChangedAction struct {
Canvases []CanvasReference `json:"canvases,omitempty"`
}
+// Requests materialization of the exact chat's Canvas type registry.
+//
+// This operation is potentially effectful: for a persisted cold chat, the host
+// may resume its backing runtime, discover extensions, and execute extension
+// startup code. It MUST NOT create a backing for a brand-new chat, send a user
+// message, or invoke a model.
+//
+// The reducer is a no-op. The host remains authoritative and publishes the
+// complete result through
+// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
+// Clients do not need to dispatch this action when
+// {@link ChatState.canvasRegistry} is already present because live provider
+// registration changes are published reactively.
+type ChatCanvasRegistryRequestedAction struct {
+ Type ActionType `json:"type"`
+}
+
+// The Canvas type registry for this exact chat changed.
+//
+// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
+// Set to `undefined` when the registry is no longer materialized or available.
+type ChatCanvasRegistryChangedAction struct {
+ Type ActionType `json:"type"`
+ // Authoritative registry, or `undefined` to clear it.
+ CanvasRegistry *CanvasRegistry `json:"canvasRegistry,omitempty"`
+}
+
// The presentation state for this canvas changed.
//
// Replaces the subscribed canvas channel state entirely. Full-replacement
@@ -1883,6 +1912,8 @@ func (*ChatBackgroundWorkRemovedAction) isStateAction() {}
func (*ChatMovableChangedAction) isStateAction() {}
func (*ChatChangesetsChangedAction) isStateAction() {}
func (*ChatCanvasesChangedAction) isStateAction() {}
+func (*ChatCanvasRegistryRequestedAction) isStateAction() {}
+func (*ChatCanvasRegistryChangedAction) isStateAction() {}
func (*CanvasStateChangedAction) isStateAction() {}
func (*SessionTitleChangedAction) isStateAction() {}
func (*ChatUsageAction) isStateAction() {}
@@ -2166,6 +2197,18 @@ func (u *StateAction) UnmarshalJSON(data []byte) error {
return err
}
u.Value = &value
+ case "chat/canvasRegistryRequested":
+ var value ChatCanvasRegistryRequestedAction
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ u.Value = &value
+ case "chat/canvasRegistryChanged":
+ var value ChatCanvasRegistryChangedAction
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ u.Value = &value
case "canvas/stateChanged":
var value CanvasStateChangedAction
if err := json.Unmarshal(data, &value); err != nil {
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index b8ddde896..8d1ff2986 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -764,34 +764,13 @@ type MoveChatResult struct {
Session URI `json:"session"`
}
-// Lists canvas types currently available to open for one exact chat.
-//
-// This read MAY lazily resume an existing persisted chat so its extension
-// registry can be reconstructed. It MUST NOT create a backing runtime for a
-// brand-new chat, send a user message, or invoke a model.
-//
-// Clients MUST NOT call this command unless the selected agent advertises
-// {@link AgentCapabilities.canvases}.
-type ListCanvasTypesParams struct {
- // Channel URI this command targets.
- Channel URI `json:"channel"`
- // Optional JSON-serializable metadata associated with this request.
- // Receivers MUST ignore keys they do not understand.
- Meta map[string]json.RawMessage `json:"_meta,omitempty"`
-}
-
-// Canvas types available to open for the requested chat.
-type ListCanvasTypesResult struct {
- // Available canvas types and their declaring extensions.
- Items []CanvasTypeDeclaration `json:"items"`
-}
-
-// Opens or focuses one stable canvas instance in an exact chat.
+// Opens or focuses one client-addressed canvas resource in an exact chat.
//
// The resulting live instance is published through the chat's
// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-// Repeating this command with the same `instanceId` focuses or refreshes that
-// logical instance rather than creating a duplicate.
+// Repeating this command with the same `channel` focuses or refreshes that
+// logical instance rather than creating a duplicate. Reusing the channel for
+// a different chat, extension, or canvas type MUST fail with `Conflict`.
//
// The null result is not presentation state. Clients MUST observe
// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -805,12 +784,12 @@ type OpenCanvasParams struct {
// Optional JSON-serializable metadata associated with this request.
// Receivers MUST ignore keys they do not understand.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
+ // Exact chat URI that owns the canvas instance.
+ Chat URI `json:"chat"`
// Stable identifier of the extension that declares the canvas.
ExtensionId string `json:"extensionId"`
// Provider-local canvas type identifier.
CanvasId string `json:"canvasId"`
- // Caller-chosen stable instance identifier.
- InstanceId string `json:"instanceId"`
}
// Returns a list of session summaries. Used to populate session lists and sidebars.
diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go
index 0c586075d..115d8cf7b 100644
--- a/clients/go/ahptypes/roundtrip_fixture_test.go
+++ b/clients/go/ahptypes/roundtrip_fixture_test.go
@@ -316,6 +316,8 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string {
return enc(&v)
case "ResourceReadResult":
var v ResourceReadResult
+ case "OpenCanvasParams":
+ var v OpenCanvasParams
dec(&v)
return enc(&v)
default:
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index 0d5936327..8255f247c 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -804,12 +804,15 @@ type AgentInfo struct {
type AgentCapabilities struct {
// Experimental Canvas API.
//
- // The agent can list canvas types for an exact chat with `listCanvasTypes`
- // and open them with `openCanvas`. Live instances are published through the
- // chat's canvas references and their `ahp-canvas:` state channels.
+ // The agent can materialize an exact chat's
+ // {@link ChatState.canvasRegistry | Canvas registry} through
+ // `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
+ // Live instances are published through the chat's canvas references and
+ // their `ahp-canvas:` state channels.
//
// Clients MUST treat absence as unsupported and SHOULD tolerate additive
- // discovery metadata while the Canvas channel remains experimental.
+ // registry metadata while the Canvas channel remains experimental. This
+ // capability does not gate server-published Canvas references.
Canvases map[string]json.RawMessage `json:"canvases,omitempty"`
// The agent can host more than one concurrent chat per session. When absent,
// clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -1461,6 +1464,15 @@ type ChatState struct {
// absent from {@link ChatSummary}; clients obtain it by subscribing to the
// chat channel.
BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"`
+ // Canvas types currently registered for this exact chat.
+ //
+ // Absence means the registry has not been materialized or is not currently
+ // available. Clients that need it MAY dispatch
+ // `chat/canvasRegistryRequested`; the host publishes the authoritative
+ // registry through `chat/canvasRegistryChanged`. An empty
+ // {@link CanvasRegistry.extensions} array is a loaded registry with no
+ // available canvas types.
+ CanvasRegistry *CanvasRegistry `json:"canvasRegistry,omitempty"`
// Live canvases currently exposed by this chat.
//
// Entries intentionally contain only subscribable channel references.
@@ -1505,7 +1517,24 @@ type ChatState struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
-// One canvas type currently available to open for an exact chat.
+// One extension/provider that declares canvas types for an exact chat.
+//
+// The extension metadata is normalized here instead of repeated on every
+// {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
+// infer its source or management behavior by parsing it.
+type CanvasExtensionDeclaration struct {
+ // Stable opaque identifier of the extension/provider.
+ ExtensionId string `json:"extensionId"`
+ // Where the runtime discovered the extension/provider.
+ ExtensionSource CanvasExtensionSource `json:"extensionSource"`
+ // Human-readable extension/provider name, when available.
+ ExtensionName *string `json:"extensionName,omitempty"`
+ // Canvas types declared by this extension/provider. `canvasId` MUST be
+ // unique within this array.
+ Canvases []CanvasTypeDeclaration `json:"canvases"`
+}
+
+// One canvas type currently available to open.
//
// This is discovery metadata, not a live canvas instance. Live instances are
// represented by {@link CanvasReference} entries on chat state and their
@@ -1516,20 +1545,23 @@ type ChatState struct {
type CanvasTypeDeclaration struct {
// Provider-local canvas type identifier.
CanvasId string `json:"canvasId"`
- // Stable identifier of the extension that declares the canvas.
- ExtensionId string `json:"extensionId"`
- // Where the runtime discovered the declaring extension.
- ExtensionSource CanvasExtensionSource `json:"extensionSource"`
- // Human-readable extension name, when available.
- ExtensionName *string `json:"extensionName,omitempty"`
// Human-readable canvas type name.
DisplayName string `json:"displayName"`
// Description of what the canvas type does.
Description string `json:"description"`
// Whether opening this canvas requires an input flow.
RequiresInput bool `json:"requiresInput"`
- // Number of actions the canvas declares.
- ActionCount int64 `json:"actionCount"`
+}
+
+// Canvas types currently registered for one exact chat.
+//
+// This is a compact presentation registry, not extension installation state
+// and not the list of live instances. The host replaces the complete registry
+// through `chat/canvasRegistryChanged` whenever provider registration changes.
+type CanvasRegistry struct {
+ // Declaring extensions/providers. `extensionId` MUST be unique within this
+ // array. An empty array is an authoritative loaded registry with no types.
+ Extensions []CanvasExtensionDeclaration `json:"extensions"`
}
// Stable reference to a subscribable canvas channel.
@@ -1548,7 +1580,11 @@ type CanvasReference struct {
// The client already knows the subscribed resource, so the state does not
// redundantly carry its channel URI.
type CanvasState struct {
- // Stable caller-supplied instance identifier.
+ // Provider-facing instance identifier.
+ //
+ // AHP clients address this instance by the subscribed `ahp-canvas:` channel
+ // URI. They MUST NOT treat this provider identifier as a second resource
+ // identity.
InstanceId string `json:"instanceId"`
// Owning extension/provider identifier.
ExtensionId string `json:"extensionId"`
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt
index dd291e656..972de8f38 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt
@@ -1082,6 +1082,12 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when
is StateActionChatCanvasesChanged ->
state.copy(canvases = action.value.canvases)
+ is StateActionChatCanvasRegistryRequested ->
+ state
+
+ is StateActionChatCanvasRegistryChanged ->
+ state.copy(canvasRegistry = action.value.canvasRegistry)
+
is StateActionChatWorkingDirectorySet -> {
val list = state.workingDirectories ?: emptyList()
if (list.contains(action.value.directory)) {
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
index 23797a196..df53faf52 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
@@ -60,6 +60,8 @@ value class ActionType(val rawValue: String) {
val CHAT_MOVABLE_CHANGED: ActionType = ActionType("chat/movableChanged")
val CHAT_CHANGESETS_CHANGED: ActionType = ActionType("chat/changesetsChanged")
val CHAT_CANVASES_CHANGED: ActionType = ActionType("chat/canvasesChanged")
+ val CHAT_CANVAS_REGISTRY_REQUESTED: ActionType = ActionType("chat/canvasRegistryRequested")
+ val CHAT_CANVAS_REGISTRY_CHANGED: ActionType = ActionType("chat/canvasRegistryChanged")
val CANVAS_STATE_CHANGED: ActionType = ActionType("canvas/stateChanged")
val CHAT_WORKING_DIRECTORY_SET: ActionType = ActionType("chat/workingDirectorySet")
val CHAT_WORKING_DIRECTORY_REMOVED: ActionType = ActionType("chat/workingDirectoryRemoved")
@@ -793,6 +795,20 @@ data class ChatCanvasesChangedAction(
val canvases: List? = null
)
+@Serializable
+data class ChatCanvasRegistryRequestedAction(
+ val type: ActionType
+)
+
+@Serializable
+data class ChatCanvasRegistryChangedAction(
+ val type: ActionType,
+ /**
+ * Authoritative registry, or `undefined` to clear it.
+ */
+ val canvasRegistry: CanvasRegistry? = null
+)
+
@Serializable
data class CanvasStateChangedAction(
val type: ActionType,
@@ -1770,6 +1786,8 @@ sealed interface StateAction
@JvmInline value class StateActionChatMovableChanged(val value: ChatMovableChangedAction) : StateAction
@JvmInline value class StateActionChatChangesetsChanged(val value: ChatChangesetsChangedAction) : StateAction
@JvmInline value class StateActionChatCanvasesChanged(val value: ChatCanvasesChangedAction) : StateAction
+@JvmInline value class StateActionChatCanvasRegistryRequested(val value: ChatCanvasRegistryRequestedAction) : StateAction
+@JvmInline value class StateActionChatCanvasRegistryChanged(val value: ChatCanvasRegistryChangedAction) : StateAction
@JvmInline value class StateActionCanvasStateChanged(val value: CanvasStateChangedAction) : StateAction
@JvmInline value class StateActionSessionTitleChanged(val value: SessionTitleChangedAction) : StateAction
@JvmInline value class StateActionChatUsage(val value: ChatUsageAction) : StateAction
@@ -1893,6 +1911,8 @@ internal object StateActionSerializer : KSerializer {
"chat/movableChanged" -> StateActionChatMovableChanged(input.json.decodeFromJsonElement(ChatMovableChangedAction.serializer(), element))
"chat/changesetsChanged" -> StateActionChatChangesetsChanged(input.json.decodeFromJsonElement(ChatChangesetsChangedAction.serializer(), element))
"chat/canvasesChanged" -> StateActionChatCanvasesChanged(input.json.decodeFromJsonElement(ChatCanvasesChangedAction.serializer(), element))
+ "chat/canvasRegistryRequested" -> StateActionChatCanvasRegistryRequested(input.json.decodeFromJsonElement(ChatCanvasRegistryRequestedAction.serializer(), element))
+ "chat/canvasRegistryChanged" -> StateActionChatCanvasRegistryChanged(input.json.decodeFromJsonElement(ChatCanvasRegistryChangedAction.serializer(), element))
"canvas/stateChanged" -> StateActionCanvasStateChanged(input.json.decodeFromJsonElement(CanvasStateChangedAction.serializer(), element))
"session/titleChanged" -> StateActionSessionTitleChanged(input.json.decodeFromJsonElement(SessionTitleChangedAction.serializer(), element))
"chat/usage" -> StateActionChatUsage(input.json.decodeFromJsonElement(ChatUsageAction.serializer(), element))
@@ -2009,6 +2029,8 @@ internal object StateActionSerializer : KSerializer {
is StateActionChatMovableChanged -> output.json.encodeToJsonElement(ChatMovableChangedAction.serializer(), value.value)
is StateActionChatChangesetsChanged -> output.json.encodeToJsonElement(ChatChangesetsChangedAction.serializer(), value.value)
is StateActionChatCanvasesChanged -> output.json.encodeToJsonElement(ChatCanvasesChangedAction.serializer(), value.value)
+ is StateActionChatCanvasRegistryRequested -> output.json.encodeToJsonElement(ChatCanvasRegistryRequestedAction.serializer(), value.value)
+ is StateActionChatCanvasRegistryChanged -> output.json.encodeToJsonElement(ChatCanvasRegistryChangedAction.serializer(), value.value)
is StateActionCanvasStateChanged -> output.json.encodeToJsonElement(CanvasStateChangedAction.serializer(), value.value)
is StateActionSessionTitleChanged -> output.json.encodeToJsonElement(SessionTitleChangedAction.serializer(), value.value)
is StateActionChatUsage -> output.json.encodeToJsonElement(ChatUsageAction.serializer(), value.value)
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index 4e85b99ad..24cd611b5 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1024,28 +1024,6 @@ data class MoveChatResult(
val session: String
)
-@Serializable
-data class ListCanvasTypesParams(
- /**
- * Channel URI this command targets.
- */
- val channel: String,
- /**
- * Optional JSON-serializable metadata associated with this request.
- * Receivers MUST ignore keys they do not understand.
- */
- @SerialName("_meta")
- val meta: Map? = null
-)
-
-@Serializable
-data class ListCanvasTypesResult(
- /**
- * Available canvas types and their declaring extensions.
- */
- val items: List
-)
-
@Serializable
data class OpenCanvasParams(
/**
@@ -1058,6 +1036,10 @@ data class OpenCanvasParams(
*/
@SerialName("_meta")
val meta: Map? = null,
+ /**
+ * Exact chat URI that owns the canvas instance.
+ */
+ val chat: String,
/**
* Stable identifier of the extension that declares the canvas.
*/
@@ -1065,11 +1047,7 @@ data class OpenCanvasParams(
/**
* Provider-local canvas type identifier.
*/
- val canvasId: String,
- /**
- * Caller-chosen stable instance identifier.
- */
- val instanceId: String
+ val canvasId: String
)
@Serializable
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 6e9890d00..6030c6c91 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1550,12 +1550,15 @@ data class AgentCapabilities(
/**
* Experimental Canvas API.
*
- * The agent can list canvas types for an exact chat with `listCanvasTypes`
- * and open them with `openCanvas`. Live instances are published through the
- * chat's canvas references and their `ahp-canvas:` state channels.
+ * The agent can materialize an exact chat's
+ * {@link ChatState.canvasRegistry | Canvas registry} through
+ * `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
+ * Live instances are published through the chat's canvas references and
+ * their `ahp-canvas:` state channels.
*
* Clients MUST treat absence as unsupported and SHOULD tolerate additive
- * discovery metadata while the Canvas channel remains experimental.
+ * registry metadata while the Canvas channel remains experimental. This
+ * capability does not gate server-published Canvas references.
*/
val canvases: Map? = null,
/**
@@ -1909,6 +1912,17 @@ data class ChatState(
* chat channel.
*/
val backgroundWork: List? = null,
+ /**
+ * Canvas types currently registered for this exact chat.
+ *
+ * Absence means the registry has not been materialized or is not currently
+ * available. Clients that need it MAY dispatch
+ * `chat/canvasRegistryRequested`; the host publishes the authoritative
+ * registry through `chat/canvasRegistryChanged`. An empty
+ * {@link CanvasRegistry.extensions} array is a loaded registry with no
+ * available canvas types.
+ */
+ val canvasRegistry: CanvasRegistry? = null,
/**
* Live canvases currently exposed by this chat.
*
@@ -2293,23 +2307,32 @@ data class BackgroundSubagentWork(
)
@Serializable
-data class CanvasTypeDeclaration(
+data class CanvasExtensionDeclaration(
/**
- * Provider-local canvas type identifier.
- */
- val canvasId: String,
- /**
- * Stable identifier of the extension that declares the canvas.
+ * Stable opaque identifier of the extension/provider.
*/
val extensionId: String,
/**
- * Where the runtime discovered the declaring extension.
+ * Where the runtime discovered the extension/provider.
*/
val extensionSource: CanvasExtensionSource,
/**
- * Human-readable extension name, when available.
+ * Human-readable extension/provider name, when available.
*/
val extensionName: String? = null,
+ /**
+ * Canvas types declared by this extension/provider. `canvasId` MUST be
+ * unique within this array.
+ */
+ val canvases: List
+)
+
+@Serializable
+data class CanvasTypeDeclaration(
+ /**
+ * Provider-local canvas type identifier.
+ */
+ val canvasId: String,
/**
* Human-readable canvas type name.
*/
@@ -2321,11 +2344,16 @@ data class CanvasTypeDeclaration(
/**
* Whether opening this canvas requires an input flow.
*/
- val requiresInput: Boolean,
+ val requiresInput: Boolean
+)
+
+@Serializable
+data class CanvasRegistry(
/**
- * Number of actions the canvas declares.
+ * Declaring extensions/providers. `extensionId` MUST be unique within this
+ * array. An empty array is an authoritative loaded registry with no types.
*/
- val actionCount: Long
+ val extensions: List
)
@Serializable
@@ -2339,7 +2367,11 @@ data class CanvasReference(
@Serializable
data class CanvasState(
/**
- * Stable caller-supplied instance identifier.
+ * Provider-facing instance identifier.
+ *
+ * AHP clients address this instance by the subscribed `ahp-canvas:` channel
+ * URI. They MUST NOT treat this provider identifier as a second resource
+ * identity.
*/
val instanceId: String,
/**
diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
index c24300907..6a8a83986 100644
--- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
+++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
@@ -47,6 +47,7 @@ import com.microsoft.agenthostprotocol.generated.TcpResetParams
import com.microsoft.agenthostprotocol.generated.CreateTcpConnectionParams
import com.microsoft.agenthostprotocol.generated.ResourceReadParams
import com.microsoft.agenthostprotocol.generated.ResourceReadResult
+import com.microsoft.agenthostprotocol.generated.OpenCanvasParams
import com.microsoft.agenthostprotocol.generated.JsonRpcErrorResponse
import com.microsoft.agenthostprotocol.generated.JsonRpcNotification
import com.microsoft.agenthostprotocol.generated.JsonRpcRequest
@@ -281,6 +282,7 @@ class RoundTripCorpusTest {
"CreateTcpConnectionParams" -> rt(CreateTcpConnectionParams.serializer())
"ResourceReadParams" -> rt(ResourceReadParams.serializer())
"ResourceReadResult" -> rt(ResourceReadResult.serializer())
+ "OpenCanvasParams" -> rt(OpenCanvasParams.serializer())
else -> fail(
"$file: unknown wire type \"$typeName\". " +
"Add a decode entry to decodeAndReencode.",
diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs
index 591daf586..1a1fc7966 100644
--- a/clients/rust/crates/ahp-types/src/actions.rs
+++ b/clients/rust/crates/ahp-types/src/actions.rs
@@ -15,8 +15,8 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::state::{
AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition,
AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary,
- BackgroundWork, CanvasReference, CanvasState, ChangesSummary, Changeset, ChangesetFile,
- ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer,
+ BackgroundWork, CanvasReference, CanvasRegistry, CanvasState, ChangesSummary, Changeset,
+ ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer,
ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary,
ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo,
ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, Message,
@@ -62,6 +62,8 @@ pub enum ActionType {
ChatMovableChanged,
ChatChangesetsChanged,
ChatCanvasesChanged,
+ ChatCanvasRegistryRequested,
+ ChatCanvasRegistryChanged,
CanvasStateChanged,
ChatWorkingDirectorySet,
ChatWorkingDirectoryRemoved,
@@ -190,6 +192,12 @@ impl serde::Serialize for ActionType {
Self::ChatMovableChanged => serializer.serialize_str("chat/movableChanged"),
Self::ChatChangesetsChanged => serializer.serialize_str("chat/changesetsChanged"),
Self::ChatCanvasesChanged => serializer.serialize_str("chat/canvasesChanged"),
+ Self::ChatCanvasRegistryRequested => {
+ serializer.serialize_str("chat/canvasRegistryRequested")
+ }
+ Self::ChatCanvasRegistryChanged => {
+ serializer.serialize_str("chat/canvasRegistryChanged")
+ }
Self::CanvasStateChanged => serializer.serialize_str("canvas/stateChanged"),
Self::ChatWorkingDirectorySet => serializer.serialize_str("chat/workingDirectorySet"),
Self::ChatWorkingDirectoryRemoved => {
@@ -364,6 +372,8 @@ impl<'de> serde::Deserialize<'de> for ActionType {
"chat/movableChanged" => Self::ChatMovableChanged,
"chat/changesetsChanged" => Self::ChatChangesetsChanged,
"chat/canvasesChanged" => Self::ChatCanvasesChanged,
+ "chat/canvasRegistryRequested" => Self::ChatCanvasRegistryRequested,
+ "chat/canvasRegistryChanged" => Self::ChatCanvasRegistryChanged,
"canvas/stateChanged" => Self::CanvasStateChanged,
"chat/workingDirectorySet" => Self::ChatWorkingDirectorySet,
"chat/workingDirectoryRemoved" => Self::ChatWorkingDirectoryRemoved,
@@ -1163,6 +1173,35 @@ pub struct ChatCanvasesChangedAction {
pub canvases: Option>,
}
+/// Requests materialization of the exact chat's Canvas type registry.
+///
+/// This operation is potentially effectful: for a persisted cold chat, the host
+/// may resume its backing runtime, discover extensions, and execute extension
+/// startup code. It MUST NOT create a backing for a brand-new chat, send a user
+/// message, or invoke a model.
+///
+/// The reducer is a no-op. The host remains authoritative and publishes the
+/// complete result through
+/// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
+/// Clients do not need to dispatch this action when
+/// {@link ChatState.canvasRegistry} is already present because live provider
+/// registration changes are published reactively.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChatCanvasRegistryRequestedAction {}
+
+/// The Canvas type registry for this exact chat changed.
+///
+/// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
+/// Set to `undefined` when the registry is no longer materialized or available.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct ChatCanvasRegistryChangedAction {
+ /// Authoritative registry, or `undefined` to clear it.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub canvas_registry: Option,
+}
+
/// The presentation state for this canvas changed.
///
/// Replaces the subscribed canvas channel state entirely. Full-replacement
@@ -2506,6 +2545,10 @@ pub enum StateAction {
ChatChangesetsChanged(ChatChangesetsChangedAction),
#[serde(rename = "chat/canvasesChanged")]
ChatCanvasesChanged(ChatCanvasesChangedAction),
+ #[serde(rename = "chat/canvasRegistryRequested")]
+ ChatCanvasRegistryRequested(ChatCanvasRegistryRequestedAction),
+ #[serde(rename = "chat/canvasRegistryChanged")]
+ ChatCanvasRegistryChanged(ChatCanvasRegistryChangedAction),
#[serde(rename = "canvas/stateChanged")]
CanvasStateChanged(CanvasStateChangedAction),
#[serde(rename = "session/titleChanged")]
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 89efc0af7..913e5bb86 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -16,11 +16,10 @@ use crate::actions::{ActionEnvelope, StateAction};
#[allow(unused_imports)]
use crate::state::{
AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate,
- AutomationTrigger, AutomationTriggerDefinition, CanvasTypeDeclaration, ChannelFlowControl,
- ChannelReceiveLimits, ContentRef, Message, MessageAttachment, ModelSelection,
- ResumedChannelSubscription, SessionActiveClient, SessionConfigSchema, SessionSummary,
- SideChatSelection, Snapshot, SnapshotState, TcpConnectionsCapability, TelemetryCapabilities,
- TerminalClaim, TextRange, Turn,
+ AutomationTrigger, AutomationTriggerDefinition, ChannelFlowControl, ChannelReceiveLimits,
+ ContentRef, Message, MessageAttachment, ModelSelection, ResumedChannelSubscription,
+ SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot,
+ SnapshotState, TcpConnectionsCapability, TelemetryCapabilities, TerminalClaim, TextRange, Turn,
};
// ─── Enums ────────────────────────────────────────────────────────────
@@ -1056,39 +1055,13 @@ pub struct MoveChatResult {
pub session: Uri,
}
-/// Lists canvas types currently available to open for one exact chat.
-///
-/// This read MAY lazily resume an existing persisted chat so its extension
-/// registry can be reconstructed. It MUST NOT create a backing runtime for a
-/// brand-new chat, send a user message, or invoke a model.
-///
-/// Clients MUST NOT call this command unless the selected agent advertises
-/// {@link AgentCapabilities.canvases}.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ListCanvasTypesParams {
- /// Channel URI this command targets.
- pub channel: Uri,
- /// Optional JSON-serializable metadata associated with this request.
- /// Receivers MUST ignore keys they do not understand.
- #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
- pub meta: Option,
-}
-
-/// Canvas types available to open for the requested chat.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ListCanvasTypesResult {
- /// Available canvas types and their declaring extensions.
- pub items: Vec,
-}
-
-/// Opens or focuses one stable canvas instance in an exact chat.
+/// Opens or focuses one client-addressed canvas resource in an exact chat.
///
/// The resulting live instance is published through the chat's
/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-/// Repeating this command with the same `instanceId` focuses or refreshes that
-/// logical instance rather than creating a duplicate.
+/// Repeating this command with the same `channel` focuses or refreshes that
+/// logical instance rather than creating a duplicate. Reusing the channel for
+/// a different chat, extension, or canvas type MUST fail with `Conflict`.
///
/// The null result is not presentation state. Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -1105,12 +1078,12 @@ pub struct OpenCanvasParams {
/// Receivers MUST ignore keys they do not understand.
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option,
+ /// Exact chat URI that owns the canvas instance.
+ pub chat: Uri,
/// Stable identifier of the extension that declares the canvas.
pub extension_id: String,
/// Provider-local canvas type identifier.
pub canvas_id: String,
- /// Caller-chosen stable instance identifier.
- pub instance_id: String,
}
/// Returns a list of session summaries. Used to populate session lists and sidebars.
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index 4999c2377..61768d190 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1825,12 +1825,15 @@ pub struct AgentInfo {
pub struct AgentCapabilities {
/// Experimental Canvas API.
///
- /// The agent can list canvas types for an exact chat with `listCanvasTypes`
- /// and open them with `openCanvas`. Live instances are published through the
- /// chat's canvas references and their `ahp-canvas:` state channels.
+ /// The agent can materialize an exact chat's
+ /// {@link ChatState.canvasRegistry | Canvas registry} through
+ /// `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
+ /// Live instances are published through the chat's canvas references and
+ /// their `ahp-canvas:` state channels.
///
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
- /// discovery metadata while the Canvas channel remains experimental.
+ /// registry metadata while the Canvas channel remains experimental. This
+ /// capability does not gate server-published Canvas references.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canvases: Option,
/// The agent can host more than one concurrent chat per session. When absent,
@@ -2166,6 +2169,16 @@ pub struct ChatState {
/// chat channel.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background_work: Option>,
+ /// Canvas types currently registered for this exact chat.
+ ///
+ /// Absence means the registry has not been materialized or is not currently
+ /// available. Clients that need it MAY dispatch
+ /// `chat/canvasRegistryRequested`; the host publishes the authoritative
+ /// registry through `chat/canvasRegistryChanged`. An empty
+ /// {@link CanvasRegistry.extensions} array is a loaded registry with no
+ /// available canvas types.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub canvas_registry: Option,
/// Live canvases currently exposed by this chat.
///
/// Entries intentionally contain only subscribable channel references.
@@ -2218,7 +2231,27 @@ pub struct ChatState {
pub meta: Option,
}
-/// One canvas type currently available to open for an exact chat.
+/// One extension/provider that declares canvas types for an exact chat.
+///
+/// The extension metadata is normalized here instead of repeated on every
+/// {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
+/// infer its source or management behavior by parsing it.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CanvasExtensionDeclaration {
+ /// Stable opaque identifier of the extension/provider.
+ pub extension_id: String,
+ /// Where the runtime discovered the extension/provider.
+ pub extension_source: CanvasExtensionSource,
+ /// Human-readable extension/provider name, when available.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub extension_name: Option,
+ /// Canvas types declared by this extension/provider. `canvasId` MUST be
+ /// unique within this array.
+ pub canvases: Vec,
+}
+
+/// One canvas type currently available to open.
///
/// This is discovery metadata, not a live canvas instance. Live instances are
/// represented by {@link CanvasReference} entries on chat state and their
@@ -2231,21 +2264,25 @@ pub struct ChatState {
pub struct CanvasTypeDeclaration {
/// Provider-local canvas type identifier.
pub canvas_id: String,
- /// Stable identifier of the extension that declares the canvas.
- pub extension_id: String,
- /// Where the runtime discovered the declaring extension.
- pub extension_source: CanvasExtensionSource,
- /// Human-readable extension name, when available.
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub extension_name: Option,
/// Human-readable canvas type name.
pub display_name: String,
/// Description of what the canvas type does.
pub description: String,
/// Whether opening this canvas requires an input flow.
pub requires_input: bool,
- /// Number of actions the canvas declares.
- pub action_count: i64,
+}
+
+/// Canvas types currently registered for one exact chat.
+///
+/// This is a compact presentation registry, not extension installation state
+/// and not the list of live instances. The host replaces the complete registry
+/// through `chat/canvasRegistryChanged` whenever provider registration changes.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CanvasRegistry {
+ /// Declaring extensions/providers. `extensionId` MUST be unique within this
+ /// array. An empty array is an authoritative loaded registry with no types.
+ pub extensions: Vec,
}
/// Stable reference to a subscribable canvas channel.
@@ -2268,7 +2305,11 @@ pub struct CanvasReference {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasState {
- /// Stable caller-supplied instance identifier.
+ /// Provider-facing instance identifier.
+ ///
+ /// AHP clients address this instance by the subscribed `ahp-canvas:` channel
+ /// URI. They MUST NOT treat this provider identifier as a second resource
+ /// identity.
pub instance_id: String,
/// Owning extension/provider identifier.
pub extension_id: String,
diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
index 5876331eb..0027952e5 100644
--- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
+++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
@@ -31,7 +31,7 @@ use ahp_types::{
commands::{
ChangesetOperationTarget, ChatSource, CreateChatParams, CreateTcpConnectionParams,
Implementation, InitializeResult, ReconnectParams, ReconnectResult, ResourceReadParams,
- ResourceReadResult, SubscribeParams, SubscribeResult,
+ OpenCanvasParams, ResourceReadResult, SubscribeParams, SubscribeResult,
},
common::StringOrMarkdown,
messages::JsonRpcMessage,
@@ -245,6 +245,7 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result<
"CreateTcpConnectionParams" => round_trip!(CreateTcpConnectionParams),
"ResourceReadParams" => round_trip!(ResourceReadParams),
"ResourceReadResult" => round_trip!(ResourceReadResult),
+ "OpenCanvasParams" => round_trip!(OpenCanvasParams),
other => Err(format!(
"{}: unknown wire type {:?}. Add a decode entry to decode_and_reencode.",
file, other
diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs
index 08ca1ebcf..a10ed9890 100644
--- a/clients/rust/crates/ahp/src/reducers.rs
+++ b/clients/rust/crates/ahp/src/reducers.rs
@@ -1221,6 +1221,11 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu
state.canvases = a.canvases.clone();
ReduceOutcome::Applied
}
+ StateAction::ChatCanvasRegistryRequested(_) => ReduceOutcome::NoOp,
+ StateAction::ChatCanvasRegistryChanged(a) => {
+ state.canvas_registry = a.canvas_registry.clone();
+ ReduceOutcome::Applied
+ }
StateAction::ChatWorkingDirectorySet(a) => {
let list = state.working_directories.get_or_insert_with(Vec::new);
if list.contains(&a.directory) {
@@ -2378,6 +2383,7 @@ mod tests {
provider: None,
working_directories: None,
changesets: None,
+ canvas_registry: None,
canvases: None,
config: None,
turns: Vec::new(),
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
index 707d69c22..3a7d7a01a 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
@@ -37,6 +37,8 @@ public enum ActionType: Codable, Sendable, Equatable {
case chatMovableChanged
case chatChangesetsChanged
case chatCanvasesChanged
+ case chatCanvasRegistryRequested
+ case chatCanvasRegistryChanged
case canvasStateChanged
case chatWorkingDirectorySet
case chatWorkingDirectoryRemoved
@@ -152,6 +154,8 @@ public enum ActionType: Codable, Sendable, Equatable {
case "chat/movableChanged": self = .chatMovableChanged
case "chat/changesetsChanged": self = .chatChangesetsChanged
case "chat/canvasesChanged": self = .chatCanvasesChanged
+ case "chat/canvasRegistryRequested": self = .chatCanvasRegistryRequested
+ case "chat/canvasRegistryChanged": self = .chatCanvasRegistryChanged
case "canvas/stateChanged": self = .canvasStateChanged
case "chat/workingDirectorySet": self = .chatWorkingDirectorySet
case "chat/workingDirectoryRemoved": self = .chatWorkingDirectoryRemoved
@@ -267,6 +271,8 @@ public enum ActionType: Codable, Sendable, Equatable {
case .chatMovableChanged: try container.encode("chat/movableChanged")
case .chatChangesetsChanged: try container.encode("chat/changesetsChanged")
case .chatCanvasesChanged: try container.encode("chat/canvasesChanged")
+ case .chatCanvasRegistryRequested: try container.encode("chat/canvasRegistryRequested")
+ case .chatCanvasRegistryChanged: try container.encode("chat/canvasRegistryChanged")
case .canvasStateChanged: try container.encode("canvas/stateChanged")
case .chatWorkingDirectorySet: try container.encode("chat/workingDirectorySet")
case .chatWorkingDirectoryRemoved: try container.encode("chat/workingDirectoryRemoved")
@@ -1297,6 +1303,30 @@ public struct ChatCanvasesChangedAction: Codable, Sendable {
}
}
+public struct ChatCanvasRegistryRequestedAction: Codable, Sendable {
+ public var type: ActionType
+
+ public init(
+ type: ActionType
+ ) {
+ self.type = type
+ }
+}
+
+public struct ChatCanvasRegistryChangedAction: Codable, Sendable {
+ public var type: ActionType
+ /// Authoritative registry, or `undefined` to clear it.
+ public var canvasRegistry: CanvasRegistry?
+
+ public init(
+ type: ActionType,
+ canvasRegistry: CanvasRegistry? = nil
+ ) {
+ self.type = type
+ self.canvasRegistry = canvasRegistry
+ }
+}
+
public struct CanvasStateChangedAction: Codable, Sendable {
public var type: ActionType
/// New authoritative canvas state.
@@ -2671,6 +2701,8 @@ public enum StateAction: Codable, Sendable {
case chatMovableChanged(ChatMovableChangedAction)
case chatChangesetsChanged(ChatChangesetsChangedAction)
case chatCanvasesChanged(ChatCanvasesChangedAction)
+ case chatCanvasRegistryRequested(ChatCanvasRegistryRequestedAction)
+ case chatCanvasRegistryChanged(ChatCanvasRegistryChangedAction)
case canvasStateChanged(CanvasStateChangedAction)
case sessionTitleChanged(SessionTitleChangedAction)
case chatUsage(ChatUsageAction)
@@ -2822,6 +2854,10 @@ public enum StateAction: Codable, Sendable {
self = .chatChangesetsChanged(try ChatChangesetsChangedAction(from: decoder))
case "chat/canvasesChanged":
self = .chatCanvasesChanged(try ChatCanvasesChangedAction(from: decoder))
+ case "chat/canvasRegistryRequested":
+ self = .chatCanvasRegistryRequested(try ChatCanvasRegistryRequestedAction(from: decoder))
+ case "chat/canvasRegistryChanged":
+ self = .chatCanvasRegistryChanged(try ChatCanvasRegistryChangedAction(from: decoder))
case "canvas/stateChanged":
self = .canvasStateChanged(try CanvasStateChangedAction(from: decoder))
case "session/titleChanged":
@@ -3014,6 +3050,8 @@ public enum StateAction: Codable, Sendable {
case .chatMovableChanged(let v): try v.encode(to: encoder)
case .chatChangesetsChanged(let v): try v.encode(to: encoder)
case .chatCanvasesChanged(let v): try v.encode(to: encoder)
+ case .chatCanvasRegistryRequested(let v): try v.encode(to: encoder)
+ case .chatCanvasRegistryChanged(let v): try v.encode(to: encoder)
case .canvasStateChanged(let v): try v.encode(to: encoder)
case .sessionTitleChanged(let v): try v.encode(to: encoder)
case .chatUsage(let v): try v.encode(to: encoder)
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index d33c34e4f..e92aa552f 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1154,71 +1154,39 @@ public struct MoveChatResult: Codable, Sendable {
}
}
-public struct ListCanvasTypesParams: Codable, Sendable {
- /// Channel URI this command targets.
- public var channel: String
- /// Optional JSON-serializable metadata associated with this request.
- /// Receivers MUST ignore keys they do not understand.
- public var meta: [String: AnyCodable]?
-
- enum CodingKeys: String, CodingKey {
- case channel
- case meta = "_meta"
- }
-
- public init(
- channel: String,
- meta: [String: AnyCodable]? = nil
- ) {
- self.channel = channel
- self.meta = meta
- }
-}
-
-public struct ListCanvasTypesResult: Codable, Sendable {
- /// Available canvas types and their declaring extensions.
- public var items: [CanvasTypeDeclaration]
-
- public init(
- items: [CanvasTypeDeclaration]
- ) {
- self.items = items
- }
-}
-
public struct OpenCanvasParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
/// Optional JSON-serializable metadata associated with this request.
/// Receivers MUST ignore keys they do not understand.
public var meta: [String: AnyCodable]?
+ /// Exact chat URI that owns the canvas instance.
+ public var chat: String
/// Stable identifier of the extension that declares the canvas.
public var extensionId: String
/// Provider-local canvas type identifier.
public var canvasId: String
- /// Caller-chosen stable instance identifier.
- public var instanceId: String
enum CodingKeys: String, CodingKey {
case channel
case meta = "_meta"
+ case chat
case extensionId
case canvasId
- case instanceId
}
public init(
channel: String,
meta: [String: AnyCodable]? = nil,
+ chat: String,
extensionId: String,
- canvasId: String,
- instanceId: String
+ canvasId: String
) {
self.channel = channel
self.meta = meta
+ self.chat = chat
self.extensionId = extensionId
self.canvasId = canvasId
- self.instanceId = instanceId
}
}
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index f65346bd5..a4c221db3 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1522,12 +1522,15 @@ public struct AgentInfo: Codable, Sendable {
public struct AgentCapabilities: Codable, Sendable {
/// Experimental Canvas API.
///
- /// The agent can list canvas types for an exact chat with `listCanvasTypes`
- /// and open them with `openCanvas`. Live instances are published through the
- /// chat's canvas references and their `ahp-canvas:` state channels.
+ /// The agent can materialize an exact chat's
+ /// {@link ChatState.canvasRegistry | Canvas registry} through
+ /// `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
+ /// Live instances are published through the chat's canvas references and
+ /// their `ahp-canvas:` state channels.
///
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
- /// discovery metadata while the Canvas channel remains experimental.
+ /// registry metadata while the Canvas channel remains experimental. This
+ /// capability does not gate server-published Canvas references.
public var canvases: [String: AnyCodable]?
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -1913,6 +1916,15 @@ public struct ChatState: Codable, Sendable {
/// absent from {@link ChatSummary}; clients obtain it by subscribing to the
/// chat channel.
public var backgroundWork: [BackgroundWork]?
+ /// Canvas types currently registered for this exact chat.
+ ///
+ /// Absence means the registry has not been materialized or is not currently
+ /// available. Clients that need it MAY dispatch
+ /// `chat/canvasRegistryRequested`; the host publishes the authoritative
+ /// registry through `chat/canvasRegistryChanged`. An empty
+ /// {@link CanvasRegistry.extensions} array is a loaded registry with no
+ /// available canvas types.
+ public var canvasRegistry: CanvasRegistry?
/// Live canvases currently exposed by this chat.
///
/// Entries intentionally contain only subscribable channel references.
@@ -1970,6 +1982,7 @@ public struct ChatState: Codable, Sendable {
case workingDirectories
case changesets
case backgroundWork
+ case canvasRegistry
case canvases
case config
case turns
@@ -1995,6 +2008,7 @@ public struct ChatState: Codable, Sendable {
workingDirectories: [String]? = nil,
changesets: [Changeset]? = nil,
backgroundWork: [BackgroundWork]? = nil,
+ canvasRegistry: CanvasRegistry? = nil,
canvases: [CanvasReference]? = nil,
config: ChatConfigState? = nil,
turns: [Turn],
@@ -2018,6 +2032,7 @@ public struct ChatState: Codable, Sendable {
self.workingDirectories = workingDirectories
self.changesets = changesets
self.backgroundWork = backgroundWork
+ self.canvasRegistry = canvasRegistry
self.canvases = canvases
self.config = config
self.turns = turns
@@ -6249,42 +6264,62 @@ public struct EmptyState: Codable, Sendable {
}
}
-public struct CanvasTypeDeclaration: Codable, Sendable {
- /// Provider-local canvas type identifier.
- public var canvasId: String
- /// Stable identifier of the extension that declares the canvas.
+public struct CanvasExtensionDeclaration: Codable, Sendable {
+ /// Stable opaque identifier of the extension/provider.
public var extensionId: String
- /// Where the runtime discovered the declaring extension.
+ /// Where the runtime discovered the extension/provider.
public var extensionSource: CanvasExtensionSource
- /// Human-readable extension name, when available.
+ /// Human-readable extension/provider name, when available.
public var extensionName: String?
+ /// Canvas types declared by this extension/provider. `canvasId` MUST be
+ /// unique within this array.
+ public var canvases: [CanvasTypeDeclaration]
+
+ public init(
+ extensionId: String,
+ extensionSource: CanvasExtensionSource,
+ extensionName: String? = nil,
+ canvases: [CanvasTypeDeclaration]
+ ) {
+ self.extensionId = extensionId
+ self.extensionSource = extensionSource
+ self.extensionName = extensionName
+ self.canvases = canvases
+ }
+}
+
+public struct CanvasTypeDeclaration: Codable, Sendable {
+ /// Provider-local canvas type identifier.
+ public var canvasId: String
/// Human-readable canvas type name.
public var displayName: String
/// Description of what the canvas type does.
public var description: String
/// Whether opening this canvas requires an input flow.
public var requiresInput: Bool
- /// Number of actions the canvas declares.
- public var actionCount: Int
public init(
canvasId: String,
- extensionId: String,
- extensionSource: CanvasExtensionSource,
- extensionName: String? = nil,
displayName: String,
description: String,
- requiresInput: Bool,
- actionCount: Int
+ requiresInput: Bool
) {
self.canvasId = canvasId
- self.extensionId = extensionId
- self.extensionSource = extensionSource
- self.extensionName = extensionName
self.displayName = displayName
self.description = description
self.requiresInput = requiresInput
- self.actionCount = actionCount
+ }
+}
+
+public struct CanvasRegistry: Codable, Sendable {
+ /// Declaring extensions/providers. `extensionId` MUST be unique within this
+ /// array. An empty array is an authoritative loaded registry with no types.
+ public var extensions: [CanvasExtensionDeclaration]
+
+ public init(
+ extensions: [CanvasExtensionDeclaration]
+ ) {
+ self.extensions = extensions
}
}
@@ -6300,7 +6335,11 @@ public struct CanvasReference: Codable, Sendable {
}
public struct CanvasState: Codable, Sendable {
- /// Stable caller-supplied instance identifier.
+ /// Provider-facing instance identifier.
+ ///
+ /// AHP clients address this instance by the subscribed `ahp-canvas:` channel
+ /// URI. They MUST NOT treat this provider identifier as a second resource
+ /// identity.
public var instanceId: String
/// Owning extension/provider identifier.
public var extensionId: String
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
index 502d8fc95..80fe05dbb 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
@@ -247,6 +247,14 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState {
next.canvases = a.canvases
return next
+ case .chatCanvasRegistryRequested:
+ return state
+
+ case .chatCanvasRegistryChanged(let a):
+ var next = state
+ next.canvasRegistry = a.canvasRegistry
+ return next
+
case .chatWorkingDirectorySet(let a):
if (state.workingDirectories ?? []).contains(a.directory) { return state }
var next = state
diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
index 6b5fd8628..6b2cc2cd4 100644
--- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
+++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
@@ -218,6 +218,8 @@ final class TypesRoundTripFixtureTests: XCTestCase {
return try reencode(dec.decode(ResourceReadParams.self, from: inputData))
case "ResourceReadResult":
return try reencode(dec.decode(ResourceReadResult.self, from: inputData))
+ case "OpenCanvasParams":
+ return try reencode(dec.decode(OpenCanvasParams.self, from: inputData))
default:
throw FixtureError.message(
"round-trip fixture: unknown wire type \"\(type)\". Add a decode entry to decodeAndReencode.")
diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts
index 39a23679e..75e7a264b 100644
--- a/clients/typescript/test/types-round-trip.test.ts
+++ b/clients/typescript/test/types-round-trip.test.ts
@@ -67,6 +67,7 @@ import type { ChannelFrameParams, ChannelCreditParams } from '../src/types/commo
import type { TcpDataParams, TcpEofParams, TcpResetParams } from '../src/types/channels-tcp/notifications.js';
import type { CreateTcpConnectionParams } from '../src/types/channels-tcp/commands.js';
import type { ChatSource, CreateChatParams } from '../src/types/channels-chat/commands.js';
+import type { OpenCanvasParams } from '../src/types/channels-canvas/commands.js';
// ─── Fixture directory ───────────────────────────────────────────────────────
@@ -264,6 +265,7 @@ function bindToType(file: string, type: string, parsed: unknown): void {
case 'CreateTcpConnectionParams': void (parsed as CreateTcpConnectionParams); break;
case 'ResourceReadParams': void (parsed as ResourceReadParams); break;
case 'ResourceReadResult': void (parsed as ResourceReadResult); break;
+ case 'OpenCanvasParams': void (parsed as OpenCanvasParams); break;
default:
throw new Error(
`${file}: unknown wire type "${type}". Add a decode entry to bindToType.`,
diff --git a/docs/.changes/20261012-canvas-discovery-commands.json b/docs/.changes/20261012-canvas-discovery-commands.json
index a4ea67f7b..6c90f1307 100644
--- a/docs/.changes/20261012-canvas-discovery-commands.json
+++ b/docs/.changes/20261012-canvas-discovery-commands.json
@@ -1,4 +1,4 @@
{
"type": "added",
- "message": "Experimental, capability-gated `listCanvasTypes` and `openCanvas` commands for discovering extension canvases and opening them in an exact chat."
+ "message": "Experimental exact-chat Canvas registry state, explicit cold-registry materialization, and URI-addressed `openCanvas` support."
}
diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md
index e84adfe25..1c8c07f25 100644
--- a/docs/specification/chat-channel.md
+++ b/docs/specification/chat-channel.md
@@ -86,6 +86,44 @@ itself changes, the server dispatches
[`chat/changesetsChanged`](/reference/chat#chatchangesetschangedaction), which
fully replaces the catalogue or clears it when `changesets` is absent.
+### Canvas type registry
+
+When the selected agent advertises
+[`AgentCapabilities.canvases`](/reference/root#agentcapabilities), an exact chat
+may expose a [`canvasRegistry`](/reference/chat#chatstate). This is the
+authoritative set of Canvas **types** the chat can currently open. It is
+separate from [`canvases`](/reference/chat#chatstate), which contains references
+to already-open live Canvas **instances**.
+
+The registry normalizes extension/provider metadata: each
+[`CanvasExtensionDeclaration`](/reference/canvas#canvasextensiondeclaration)
+appears once and carries its own Canvas types. Clients treat `extensionId` as
+opaque, key a type by `(extensionId, canvasId)`, and do not infer source or
+management behavior from the identifier's text.
+
+For a live chat, hosts keep this registry synchronized with provider
+registration and dispatch
+[`chat/canvasRegistryChanged`](/reference/chat#chatcanvasregistrychangedaction)
+only when the authoritative set changes. Clients do not poll the list or use
+`ChatState.canvases` changes as a registry-invalidation signal.
+
+Absence of `canvasRegistry` means it has not been materialized or is not
+currently available. A client may dispatch
+[`chat/canvasRegistryRequested`](/reference/chat#chatcanvasregistryrequestedaction)
+to request materialization. This action is explicitly effectful: for a
+persisted cold chat, the host may resume its backing runtime, discover
+extensions, and execute extension startup code. The host MUST NOT create a
+backing for a brand-new chat, send a user message, or invoke a model. The
+action's reducer is a no-op; the host publishes the complete resulting registry
+through `chat/canvasRegistryChanged`.
+
+To open a no-input declaration, the client sends
+[`openCanvas`](/reference/canvas#opencanvas) with a client-chosen
+`ahp-canvas:` URI in `channel`, the exact owning chat URI, and the declaration's
+`extensionId` and `canvasId`. Reusing the same Canvas URI focuses or refreshes
+that logical instance. Live membership and presentation then flow through
+`ChatState.canvases` and the Canvas channel.
+
### Per-chat configuration
[`ChatState.config`](/reference/chat#chatstate) holds the chat's own values for
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index e6ddf357f..5ef63b516 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -1460,6 +1460,34 @@
"type"
]
},
+ "ChatCanvasRegistryRequestedAction": {
+ "type": "object",
+ "description": "Requests materialization of the exact chat's Canvas type registry.\n\nThis operation is potentially effectful: for a persisted cold chat, the host\nmay resume its backing runtime, discover extensions, and execute extension\nstartup code. It MUST NOT create a backing for a brand-new chat, send a user\nmessage, or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present because live provider\nregistration changes are published reactively.",
+ "properties": {
+ "type": {
+ "const": "chat/canvasRegistryRequested"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "ChatCanvasRegistryChangedAction": {
+ "type": "object",
+ "description": "The Canvas type registry for this exact chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.",
+ "properties": {
+ "type": {
+ "const": "chat/canvasRegistryChanged"
+ },
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Authoritative registry, or `undefined` to clear it."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
"ChatWorkingDirectorySetAction": {
"type": "object",
"description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
@@ -3341,7 +3369,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -5301,6 +5329,10 @@
},
"description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}.\n\nLike {@link ChatState.changesets | changesets}, this is intentionally\nabsent from {@link ChatSummary}; clients obtain it by subscribing to the\nchat channel."
},
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ },
"canvases": {
"type": "array",
"items": {
@@ -7482,25 +7514,43 @@
"title"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasExtensionDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
"properties": {
- "canvasId": {
- "type": "string",
- "description": "Provider-local canvas type identifier."
- },
"extensionId": {
"type": "string",
- "description": "Stable identifier of the extension that declares the canvas."
+ "description": "Stable opaque identifier of the extension/provider."
},
"extensionSource": {
"$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the declaring extension."
+ "description": "Where the runtime discovered the extension/provider."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension name, when available."
+ "description": "Human-readable extension/provider name, when available."
+ },
+ "canvases": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ }
+ },
+ "required": [
+ "extensionId",
+ "extensionSource",
+ "canvases"
+ ]
+ },
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
},
"displayName": {
"type": "string",
@@ -7513,20 +7563,29 @@
"requiresInput": {
"type": "boolean",
"description": "Whether opening this canvas requires an input flow."
- },
- "actionCount": {
- "type": "number",
- "description": "Number of actions the canvas declares."
}
},
"required": [
"canvasId",
- "extensionId",
- "extensionSource",
"displayName",
"description",
- "requiresInput",
- "actionCount"
+ "requiresInput"
+ ]
+ },
+ "CanvasRegistry": {
+ "type": "object",
+ "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasExtensionDeclaration"
+ },
+ "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ }
+ },
+ "required": [
+ "extensions"
]
},
"CanvasReference": {
@@ -7548,7 +7607,7 @@
"properties": {
"instanceId": {
"type": "string",
- "description": "Stable caller-supplied instance identifier."
+ "description": "Provider-facing instance identifier.\n\nAHP clients address this instance by the subscribed `ahp-canvas:` channel\nURI. They MUST NOT treat this provider identifier as a second resource\nidentity."
},
"extensionId": {
"type": "string",
@@ -9564,6 +9623,12 @@
{
"$ref": "#/$defs/ChatCanvasesChangedAction"
},
+ {
+ "$ref": "#/$defs/ChatCanvasRegistryRequestedAction"
+ },
+ {
+ "$ref": "#/$defs/ChatCanvasRegistryChangedAction"
+ },
{
"$ref": "#/$defs/CanvasStateChangedAction"
},
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index a8303ee71..be3f70ffa 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -1576,53 +1576,23 @@
"channel"
]
},
- "ListCanvasTypesParams": {
+ "OpenCanvasParams": {
"type": "object",
- "description": "Lists canvas types currently available to open for one exact chat.\n\nThis read MAY lazily resume an existing persisted chat so its extension\nregistry can be reconstructed. It MUST NOT create a backing runtime for a\nbrand-new chat, send a user message, or invoke a model.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Opens or focuses one client-addressed canvas resource in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` focuses or refreshes that\nlogical instance rather than creating a duplicate. Reusing the channel for\na different chat, extension, or canvas type MUST fail with `Conflict`.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
- "description": "Exact chat URI whose extension registry should be queried."
+ "description": "Client-chosen URI for the canvas resource, for example\n`ahp-canvas:/`."
},
"_meta": {
"type": "object",
"additionalProperties": {},
"description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
- }
- },
- "required": [
- "channel"
- ]
- },
- "ListCanvasTypesResult": {
- "type": "object",
- "description": "Canvas types available to open for the requested chat.",
- "properties": {
- "items": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
- },
- "description": "Available canvas types and their declaring extensions."
- }
- },
- "required": [
- "items"
- ]
- },
- "OpenCanvasParams": {
- "type": "object",
- "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
- "properties": {
- "channel": {
+ },
+ "chat": {
"$ref": "#/$defs/URI",
"description": "Exact chat URI that owns the canvas instance."
},
- "_meta": {
- "type": "object",
- "additionalProperties": {},
- "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
- },
"extensionId": {
"type": "string",
"description": "Stable identifier of the extension that declares the canvas."
@@ -1630,17 +1600,13 @@
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
- },
- "instanceId": {
- "type": "string",
- "description": "Caller-chosen stable instance identifier."
}
},
"required": [
"channel",
+ "chat",
"extensionId",
- "canvasId",
- "instanceId"
+ "canvasId"
]
},
"CreateTerminalParams": {
@@ -2708,7 +2674,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -4668,6 +4634,10 @@
},
"description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}.\n\nLike {@link ChatState.changesets | changesets}, this is intentionally\nabsent from {@link ChatSummary}; clients obtain it by subscribing to the\nchat channel."
},
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ },
"canvases": {
"type": "array",
"items": {
@@ -6849,25 +6819,43 @@
"title"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasExtensionDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
"properties": {
- "canvasId": {
- "type": "string",
- "description": "Provider-local canvas type identifier."
- },
"extensionId": {
"type": "string",
- "description": "Stable identifier of the extension that declares the canvas."
+ "description": "Stable opaque identifier of the extension/provider."
},
"extensionSource": {
"$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the declaring extension."
+ "description": "Where the runtime discovered the extension/provider."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension name, when available."
+ "description": "Human-readable extension/provider name, when available."
+ },
+ "canvases": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ }
+ },
+ "required": [
+ "extensionId",
+ "extensionSource",
+ "canvases"
+ ]
+ },
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
},
"displayName": {
"type": "string",
@@ -6880,20 +6868,29 @@
"requiresInput": {
"type": "boolean",
"description": "Whether opening this canvas requires an input flow."
- },
- "actionCount": {
- "type": "number",
- "description": "Number of actions the canvas declares."
}
},
"required": [
"canvasId",
- "extensionId",
- "extensionSource",
"displayName",
"description",
- "requiresInput",
- "actionCount"
+ "requiresInput"
+ ]
+ },
+ "CanvasRegistry": {
+ "type": "object",
+ "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasExtensionDeclaration"
+ },
+ "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ }
+ },
+ "required": [
+ "extensions"
]
},
"CanvasReference": {
@@ -6915,7 +6912,7 @@
"properties": {
"instanceId": {
"type": "string",
- "description": "Stable caller-supplied instance identifier."
+ "description": "Provider-facing instance identifier.\n\nAHP clients address this instance by the subscribed `ahp-canvas:` channel\nURI. They MUST NOT treat this provider identifier as a second resource\nidentity."
},
"extensionId": {
"type": "string",
@@ -9645,6 +9642,34 @@
"type"
]
},
+ "ChatCanvasRegistryRequestedAction": {
+ "type": "object",
+ "description": "Requests materialization of the exact chat's Canvas type registry.\n\nThis operation is potentially effectful: for a persisted cold chat, the host\nmay resume its backing runtime, discover extensions, and execute extension\nstartup code. It MUST NOT create a backing for a brand-new chat, send a user\nmessage, or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present because live provider\nregistration changes are published reactively.",
+ "properties": {
+ "type": {
+ "const": "chat/canvasRegistryRequested"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "ChatCanvasRegistryChangedAction": {
+ "type": "object",
+ "description": "The Canvas type registry for this exact chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.",
+ "properties": {
+ "type": {
+ "const": "chat/canvasRegistryChanged"
+ },
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Authoritative registry, or `undefined` to clear it."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
"ChatWorkingDirectorySetAction": {
"type": "object",
"description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
@@ -10947,6 +10972,12 @@
{
"$ref": "#/$defs/ChatCanvasesChangedAction"
},
+ {
+ "$ref": "#/$defs/ChatCanvasRegistryRequestedAction"
+ },
+ {
+ "$ref": "#/$defs/ChatCanvasRegistryChangedAction"
+ },
{
"$ref": "#/$defs/CanvasStateChangedAction"
},
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 564e9725c..29926fc41 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -715,7 +715,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -2675,6 +2675,10 @@
},
"description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}.\n\nLike {@link ChatState.changesets | changesets}, this is intentionally\nabsent from {@link ChatSummary}; clients obtain it by subscribing to the\nchat channel."
},
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ },
"canvases": {
"type": "array",
"items": {
@@ -4856,25 +4860,43 @@
"title"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasExtensionDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
"properties": {
- "canvasId": {
- "type": "string",
- "description": "Provider-local canvas type identifier."
- },
"extensionId": {
"type": "string",
- "description": "Stable identifier of the extension that declares the canvas."
+ "description": "Stable opaque identifier of the extension/provider."
},
"extensionSource": {
"$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the declaring extension."
+ "description": "Where the runtime discovered the extension/provider."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension name, when available."
+ "description": "Human-readable extension/provider name, when available."
+ },
+ "canvases": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ }
+ },
+ "required": [
+ "extensionId",
+ "extensionSource",
+ "canvases"
+ ]
+ },
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
},
"displayName": {
"type": "string",
@@ -4887,20 +4909,29 @@
"requiresInput": {
"type": "boolean",
"description": "Whether opening this canvas requires an input flow."
- },
- "actionCount": {
- "type": "number",
- "description": "Number of actions the canvas declares."
}
},
"required": [
"canvasId",
- "extensionId",
- "extensionSource",
"displayName",
"description",
- "requiresInput",
- "actionCount"
+ "requiresInput"
+ ]
+ },
+ "CanvasRegistry": {
+ "type": "object",
+ "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasExtensionDeclaration"
+ },
+ "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ }
+ },
+ "required": [
+ "extensions"
]
},
"CanvasReference": {
@@ -4922,7 +4953,7 @@
"properties": {
"instanceId": {
"type": "string",
- "description": "Stable caller-supplied instance identifier."
+ "description": "Provider-facing instance identifier.\n\nAHP clients address this instance by the subscribed `ahp-canvas:` channel\nURI. They MUST NOT treat this provider identifier as a second resource\nidentity."
},
"extensionId": {
"type": "string",
@@ -7768,53 +7799,23 @@
"channel"
]
},
- "ListCanvasTypesParams": {
+ "OpenCanvasParams": {
"type": "object",
- "description": "Lists canvas types currently available to open for one exact chat.\n\nThis read MAY lazily resume an existing persisted chat so its extension\nregistry can be reconstructed. It MUST NOT create a backing runtime for a\nbrand-new chat, send a user message, or invoke a model.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Opens or focuses one client-addressed canvas resource in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` focuses or refreshes that\nlogical instance rather than creating a duplicate. Reusing the channel for\na different chat, extension, or canvas type MUST fail with `Conflict`.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
- "description": "Exact chat URI whose extension registry should be queried."
+ "description": "Client-chosen URI for the canvas resource, for example\n`ahp-canvas:/`."
},
"_meta": {
"type": "object",
"additionalProperties": {},
"description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
- }
- },
- "required": [
- "channel"
- ]
- },
- "ListCanvasTypesResult": {
- "type": "object",
- "description": "Canvas types available to open for the requested chat.",
- "properties": {
- "items": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
- },
- "description": "Available canvas types and their declaring extensions."
- }
- },
- "required": [
- "items"
- ]
- },
- "OpenCanvasParams": {
- "type": "object",
- "description": "Opens or focuses one stable canvas instance in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `instanceId` focuses or refreshes that\nlogical instance rather than creating a duplicate.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
- "properties": {
- "channel": {
+ },
+ "chat": {
"$ref": "#/$defs/URI",
"description": "Exact chat URI that owns the canvas instance."
},
- "_meta": {
- "type": "object",
- "additionalProperties": {},
- "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
- },
"extensionId": {
"type": "string",
"description": "Stable identifier of the extension that declares the canvas."
@@ -7822,17 +7823,13 @@
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
- },
- "instanceId": {
- "type": "string",
- "description": "Caller-chosen stable instance identifier."
}
},
"required": [
"channel",
+ "chat",
"extensionId",
- "canvasId",
- "instanceId"
+ "canvasId"
]
},
"CreateTerminalParams": {
@@ -9144,6 +9141,12 @@
{
"$ref": "#/$defs/ChatCanvasesChangedAction"
},
+ {
+ "$ref": "#/$defs/ChatCanvasRegistryRequestedAction"
+ },
+ {
+ "$ref": "#/$defs/ChatCanvasRegistryChangedAction"
+ },
{
"$ref": "#/$defs/CanvasStateChangedAction"
},
@@ -10738,6 +10741,34 @@
"type"
]
},
+ "ChatCanvasRegistryRequestedAction": {
+ "type": "object",
+ "description": "Requests materialization of the exact chat's Canvas type registry.\n\nThis operation is potentially effectful: for a persisted cold chat, the host\nmay resume its backing runtime, discover extensions, and execute extension\nstartup code. It MUST NOT create a backing for a brand-new chat, send a user\nmessage, or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present because live provider\nregistration changes are published reactively.",
+ "properties": {
+ "type": {
+ "const": "chat/canvasRegistryRequested"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "ChatCanvasRegistryChangedAction": {
+ "type": "object",
+ "description": "The Canvas type registry for this exact chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.",
+ "properties": {
+ "type": {
+ "const": "chat/canvasRegistryChanged"
+ },
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Authoritative registry, or `undefined` to clear it."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
"CanvasStateChangedAction": {
"type": "object",
"description": "The presentation state for this canvas changed.\n\nReplaces the subscribed canvas channel state entirely. Full-replacement\nsemantics intentionally keep this early-development channel free to evolve\nwithout expanding the stable chat action surface.",
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index 4a590568b..e58ff713a 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -1017,7 +1017,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -2977,6 +2977,10 @@
},
"description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}.\n\nLike {@link ChatState.changesets | changesets}, this is intentionally\nabsent from {@link ChatSummary}; clients obtain it by subscribing to the\nchat channel."
},
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ },
"canvases": {
"type": "array",
"items": {
@@ -5158,25 +5162,43 @@
"title"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasExtensionDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
"properties": {
- "canvasId": {
- "type": "string",
- "description": "Provider-local canvas type identifier."
- },
"extensionId": {
"type": "string",
- "description": "Stable identifier of the extension that declares the canvas."
+ "description": "Stable opaque identifier of the extension/provider."
},
"extensionSource": {
"$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the declaring extension."
+ "description": "Where the runtime discovered the extension/provider."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension name, when available."
+ "description": "Human-readable extension/provider name, when available."
+ },
+ "canvases": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ }
+ },
+ "required": [
+ "extensionId",
+ "extensionSource",
+ "canvases"
+ ]
+ },
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
},
"displayName": {
"type": "string",
@@ -5189,20 +5211,29 @@
"requiresInput": {
"type": "boolean",
"description": "Whether opening this canvas requires an input flow."
- },
- "actionCount": {
- "type": "number",
- "description": "Number of actions the canvas declares."
}
},
"required": [
"canvasId",
- "extensionId",
- "extensionSource",
"displayName",
"description",
- "requiresInput",
- "actionCount"
+ "requiresInput"
+ ]
+ },
+ "CanvasRegistry": {
+ "type": "object",
+ "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasExtensionDeclaration"
+ },
+ "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ }
+ },
+ "required": [
+ "extensions"
]
},
"CanvasReference": {
@@ -5224,7 +5255,7 @@
"properties": {
"instanceId": {
"type": "string",
- "description": "Stable caller-supplied instance identifier."
+ "description": "Provider-facing instance identifier.\n\nAHP clients address this instance by the subscribed `ahp-canvas:` channel\nURI. They MUST NOT treat this provider identifier as a second resource\nidentity."
},
"extensionId": {
"type": "string",
diff --git a/schema/state.schema.json b/schema/state.schema.json
index 36d3c80a9..b7ddf9fa8 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -626,7 +626,7 @@
"canvases": {
"type": "object",
"additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can list canvas types for an exact chat with `listCanvasTypes`\nand open them with `openCanvas`. Live instances are published through the\nchat's canvas references and their `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\ndiscovery metadata while the Canvas channel remains experimental."
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -2586,6 +2586,10 @@
},
"description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}.\n\nLike {@link ChatState.changesets | changesets}, this is intentionally\nabsent from {@link ChatSummary}; clients obtain it by subscribing to the\nchat channel."
},
+ "canvasRegistry": {
+ "$ref": "#/$defs/CanvasRegistry",
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ },
"canvases": {
"type": "array",
"items": {
@@ -4767,25 +4771,43 @@
"title"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasExtensionDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open for an exact chat.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
"properties": {
- "canvasId": {
- "type": "string",
- "description": "Provider-local canvas type identifier."
- },
"extensionId": {
"type": "string",
- "description": "Stable identifier of the extension that declares the canvas."
+ "description": "Stable opaque identifier of the extension/provider."
},
"extensionSource": {
"$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the declaring extension."
+ "description": "Where the runtime discovered the extension/provider."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension name, when available."
+ "description": "Human-readable extension/provider name, when available."
+ },
+ "canvases": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasTypeDeclaration"
+ },
+ "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ }
+ },
+ "required": [
+ "extensionId",
+ "extensionSource",
+ "canvases"
+ ]
+ },
+ "CanvasTypeDeclaration": {
+ "type": "object",
+ "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "properties": {
+ "canvasId": {
+ "type": "string",
+ "description": "Provider-local canvas type identifier."
},
"displayName": {
"type": "string",
@@ -4798,20 +4820,29 @@
"requiresInput": {
"type": "boolean",
"description": "Whether opening this canvas requires an input flow."
- },
- "actionCount": {
- "type": "number",
- "description": "Number of actions the canvas declares."
}
},
"required": [
"canvasId",
- "extensionId",
- "extensionSource",
"displayName",
"description",
- "requiresInput",
- "actionCount"
+ "requiresInput"
+ ]
+ },
+ "CanvasRegistry": {
+ "type": "object",
+ "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/CanvasExtensionDeclaration"
+ },
+ "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ }
+ },
+ "required": [
+ "extensions"
]
},
"CanvasReference": {
@@ -4833,7 +4864,7 @@
"properties": {
"instanceId": {
"type": "string",
- "description": "Stable caller-supplied instance identifier."
+ "description": "Provider-facing instance identifier.\n\nAHP clients address this instance by the subscribed `ahp-canvas:` channel\nURI. They MUST NOT treat this provider identifier as a second resource\nidentity."
},
"extensionId": {
"type": "string",
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index 1a86fed39..2c1d59fd6 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -793,7 +793,9 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin
{ name: 'BackgroundShellWork' },
{ name: 'BackgroundSubagentWork' },
{ name: 'ChatState', mutable: true },
+ { name: 'CanvasExtensionDeclaration' },
{ name: 'CanvasTypeDeclaration' },
+ { name: 'CanvasRegistry' },
{ name: 'CanvasReference' },
{ name: 'CanvasState', mutable: true },
{ name: 'ChatInputOption' },
@@ -1701,6 +1703,8 @@ const ACTION_VARIANTS: { type: string; variantName: string; tsInterface: string
{ type: 'chat/movableChanged', variantName: 'ChatMovableChanged', tsInterface: 'ChatMovableChangedAction' },
{ type: 'chat/changesetsChanged', variantName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' },
{ type: 'chat/canvasesChanged', variantName: 'ChatCanvasesChanged', tsInterface: 'ChatCanvasesChangedAction' },
+ { type: 'chat/canvasRegistryRequested', variantName: 'ChatCanvasRegistryRequested', tsInterface: 'ChatCanvasRegistryRequestedAction' },
+ { type: 'chat/canvasRegistryChanged', variantName: 'ChatCanvasRegistryChanged', tsInterface: 'ChatCanvasRegistryChangedAction' },
{ type: 'canvas/stateChanged', variantName: 'CanvasStateChanged', tsInterface: 'CanvasStateChangedAction' },
{ type: 'chat/workingDirectorySet', variantName: 'ChatWorkingDirectorySet', tsInterface: 'ChatWorkingDirectorySetAction' },
{ type: 'chat/workingDirectoryRemoved', variantName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' },
@@ -2334,7 +2338,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str
{ name: 'CreateChatParams' },
{ name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination' }, { name: 'ChatMoveToNewSessionDestination' }, { name: 'MoveChatParams' }, { name: 'MoveChatResult' },
- { name: 'ListCanvasTypesParams' }, { name: 'ListCanvasTypesResult' }, { name: 'OpenCanvasParams' },
+ { name: 'OpenCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index d8418a33c..1748df426 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -766,7 +766,9 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin
{ name: 'SessionChatSummary' },
{ name: 'ChangesSummary' },
{ name: 'ChatState' },
+ { name: 'CanvasExtensionDeclaration' },
{ name: 'CanvasTypeDeclaration' },
+ { name: 'CanvasRegistry' },
{ name: 'CanvasReference' },
{ name: 'CanvasState' },
{ name: 'ChatSummary' },
@@ -1619,6 +1621,8 @@ const ACTION_VARIANTS: {
{ type: 'chat/movableChanged', variantName: 'ChatMovableChanged', tsInterface: 'ChatMovableChangedAction' },
{ type: 'chat/changesetsChanged', variantName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' },
{ type: 'chat/canvasesChanged', variantName: 'ChatCanvasesChanged', tsInterface: 'ChatCanvasesChangedAction' },
+ { type: 'chat/canvasRegistryRequested', variantName: 'ChatCanvasRegistryRequested', tsInterface: 'ChatCanvasRegistryRequestedAction' },
+ { type: 'chat/canvasRegistryChanged', variantName: 'ChatCanvasRegistryChanged', tsInterface: 'ChatCanvasRegistryChangedAction' },
{ type: 'canvas/stateChanged', variantName: 'CanvasStateChanged', tsInterface: 'CanvasStateChangedAction' },
{ type: 'session/titleChanged', variantName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' },
{ type: 'chat/usage', variantName: 'ChatUsage', tsInterface: 'ChatUsageAction' },
@@ -1818,7 +1822,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str
{ name: 'DisposeSessionParams' },
{ name: 'ForkChatSource' }, { name: 'SideChatSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination' }, { name: 'ChatMoveToNewSessionDestination' }, { name: 'MoveChatParams' }, { name: 'MoveChatResult' },
- { name: 'ListCanvasTypesParams' }, { name: 'ListCanvasTypesResult' }, { name: 'OpenCanvasParams' },
+ { name: 'OpenCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index f0e1b5c8c..3917bbcf3 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -1013,7 +1013,7 @@ const STATE_STRUCTS = [
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'BackgroundShellWork', 'BackgroundSubagentWork',
- 'CanvasTypeDeclaration', 'CanvasReference', 'CanvasState',
+ 'CanvasExtensionDeclaration', 'CanvasTypeDeclaration', 'CanvasRegistry', 'CanvasReference', 'CanvasState',
'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest',
'SessionToolAuthenticationRequest',
'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'ChatConfigState', 'Turn', 'ActiveTurn', 'Message',
@@ -1546,6 +1546,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[]
{ type: 'chat/movableChanged', caseName: 'ChatMovableChanged', tsInterface: 'ChatMovableChangedAction' },
{ type: 'chat/changesetsChanged', caseName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' },
{ type: 'chat/canvasesChanged', caseName: 'ChatCanvasesChanged', tsInterface: 'ChatCanvasesChangedAction' },
+ { type: 'chat/canvasRegistryRequested', caseName: 'ChatCanvasRegistryRequested', tsInterface: 'ChatCanvasRegistryRequestedAction' },
+ { type: 'chat/canvasRegistryChanged', caseName: 'ChatCanvasRegistryChanged', tsInterface: 'ChatCanvasRegistryChangedAction' },
{ type: 'canvas/stateChanged', caseName: 'CanvasStateChanged', tsInterface: 'CanvasStateChangedAction' },
{ type: 'session/titleChanged', caseName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' },
{ type: 'chat/usage', caseName: 'ChatUsage', tsInterface: 'ChatUsageAction' },
@@ -1798,7 +1800,7 @@ const COMMAND_STRUCTS = [
'CreateSessionParams', 'DisposeSessionParams',
'CreateChatParams', 'DisposeChatParams',
'ChatMoveToSessionDestination', 'ChatMoveToNewSessionDestination', 'MoveChatParams', 'MoveChatResult',
- 'ListCanvasTypesParams', 'ListCanvasTypesResult', 'OpenCanvasParams',
+ 'OpenCanvasParams',
'ListSessionsParams', 'ListSessionsResult',
'ResourceReadParams', 'ResourceReadResult',
'ResourceWriteParams', 'ResourceWriteResult',
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index b08c46f4b..1f4d4f479 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -888,7 +888,9 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str
{ name: 'ConfigSchema' },
{ name: 'PendingMessage' },
{ name: 'ChatState' },
+ { name: 'CanvasExtensionDeclaration' },
{ name: 'CanvasTypeDeclaration' },
+ { name: 'CanvasRegistry' },
{ name: 'CanvasReference' },
{ name: 'CanvasState' },
{ name: 'ChatSummary' },
@@ -1585,6 +1587,8 @@ const ACTION_VARIANTS: {
{ type: 'chat/movableChanged', variantName: 'ChatMovableChanged', tsInterface: 'ChatMovableChangedAction' },
{ type: 'chat/changesetsChanged', variantName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' },
{ type: 'chat/canvasesChanged', variantName: 'ChatCanvasesChanged', tsInterface: 'ChatCanvasesChangedAction' },
+ { type: 'chat/canvasRegistryRequested', variantName: 'ChatCanvasRegistryRequested', tsInterface: 'ChatCanvasRegistryRequestedAction' },
+ { type: 'chat/canvasRegistryChanged', variantName: 'ChatCanvasRegistryChanged', tsInterface: 'ChatCanvasRegistryChangedAction' },
{ type: 'canvas/stateChanged', variantName: 'CanvasStateChanged', tsInterface: 'CanvasStateChangedAction' },
{ type: 'session/titleChanged', variantName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' },
{ type: 'chat/usage', variantName: 'ChatUsage', tsInterface: 'ChatUsageAction' },
@@ -1738,7 +1742,7 @@ impl Serialize for ChatErrorAction {
function generateActionsFile(project: Project): string {
const lines: string[] = [GENERATED_HEADER];
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, CanvasReference, CanvasState, ChangesSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionConfigSchema, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};');
+ lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, CanvasReference, CanvasRegistry, CanvasState, ChangesSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionConfigSchema, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};');
lines.push('');
// ActionType enum
lines.push('// ─── ActionType ──────────────────────────────────────────────────────\n');
@@ -1865,7 +1869,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s
{ name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination', omitDiscriminants: true }, { name: 'ChatMoveToNewSessionDestination', omitDiscriminants: true },
{ name: 'MoveChatParams' }, { name: 'MoveChatResult' },
- { name: 'ListCanvasTypesParams' }, { name: 'ListCanvasTypesResult' }, { name: 'OpenCanvasParams' },
+ { name: 'OpenCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
@@ -1938,7 +1942,7 @@ function generateCommandsFile(project: Project): string {
lines.push('#[allow(unused_imports)]');
lines.push('use crate::actions::{ActionEnvelope, StateAction};');
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, CanvasTypeDeclaration, ChannelFlowControl, ChannelReceiveLimits, ContentRef, Message, MessageAttachment, ModelSelection, ResumedChannelSubscription, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TcpConnectionsCapability, TelemetryCapabilities, TerminalClaim, TextRange, Turn};');
+ lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ChannelFlowControl, ChannelReceiveLimits, ContentRef, Message, MessageAttachment, ModelSelection, ResumedChannelSubscription, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TcpConnectionsCapability, TelemetryCapabilities, TerminalClaim, TextRange, Turn};');
lines.push('');
lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n');
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index a32edff88..444eb05f2 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -751,7 +751,7 @@ const STATE_STRUCTS = [
'TerminalRunningLifecycleState', 'TerminalExitedLifecycleState', 'TerminalState',
'TerminalUnclassifiedPart', 'TerminalCommandPart',
'UsageInfo', 'ErrorInfo', 'Snapshot', 'EmptyState',
- 'CanvasTypeDeclaration', 'CanvasReference', 'CanvasState',
+ 'CanvasExtensionDeclaration', 'CanvasTypeDeclaration', 'CanvasRegistry', 'CanvasReference', 'CanvasState',
'Changeset', 'ChangesetCapabilities', 'ChangesetState', 'ChangesetFile', 'ChangesetOperation',
'AnnotationsSummary', 'AnnotationsState', 'AnnotationOrigin', 'Annotation', 'AnnotationEntry',
'TelemetryCapabilities',
@@ -1459,6 +1459,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[]
{ type: 'chat/movableChanged', caseName: 'chatMovableChanged', tsInterface: 'ChatMovableChangedAction' },
{ type: 'chat/changesetsChanged', caseName: 'chatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' },
{ type: 'chat/canvasesChanged', caseName: 'chatCanvasesChanged', tsInterface: 'ChatCanvasesChangedAction' },
+ { type: 'chat/canvasRegistryRequested', caseName: 'chatCanvasRegistryRequested', tsInterface: 'ChatCanvasRegistryRequestedAction' },
+ { type: 'chat/canvasRegistryChanged', caseName: 'chatCanvasRegistryChanged', tsInterface: 'ChatCanvasRegistryChangedAction' },
{ type: 'canvas/stateChanged', caseName: 'canvasStateChanged', tsInterface: 'CanvasStateChangedAction' },
{ type: 'session/titleChanged', caseName: 'sessionTitleChanged', tsInterface: 'SessionTitleChangedAction' },
{ type: 'chat/usage', caseName: 'chatUsage', tsInterface: 'ChatUsageAction' },
@@ -1720,7 +1722,7 @@ const COMMAND_STRUCTS = [
'CreateSessionParams', 'DisposeSessionParams',
'CreateChatParams', 'DisposeChatParams',
'ChatMoveToSessionDestination', 'ChatMoveToNewSessionDestination', 'MoveChatParams', 'MoveChatResult',
- 'ListCanvasTypesParams', 'ListCanvasTypesResult', 'OpenCanvasParams',
+ 'OpenCanvasParams',
'ListSessionsParams', 'ListSessionsResult',
'ResourceReadParams', 'ResourceReadResult',
'ResourceWriteParams', 'ResourceWriteResult',
diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts
index f3f6281d7..3fbb01ec5 100644
--- a/types/action-origin.generated.ts
+++ b/types/action-origin.generated.ts
@@ -59,6 +59,8 @@ import type {
ChatMovableChangedAction,
ChatChangesetsChangedAction,
ChatCanvasesChangedAction,
+ ChatCanvasRegistryRequestedAction,
+ ChatCanvasRegistryChangedAction,
CanvasStateChangedAction,
ChatWorkingDirectorySetAction,
ChatWorkingDirectoryRemovedAction,
@@ -233,6 +235,8 @@ export type ChatAction =
| ChatMovableChangedAction
| ChatChangesetsChangedAction
| ChatCanvasesChangedAction
+ | ChatCanvasRegistryRequestedAction
+ | ChatCanvasRegistryChangedAction
| ChatWorkingDirectorySetAction
| ChatWorkingDirectoryRemovedAction
| ChatUsageAction
@@ -261,6 +265,7 @@ export type ClientChatAction =
| ChatToolCallContentChangedAction
| ChatTurnCancelledAction
| ChatTurnResumeAction
+ | ChatCanvasRegistryRequestedAction
| ChatWorkingDirectorySetAction
| ChatWorkingDirectoryRemovedAction
| ChatPendingMessageSetAction
@@ -292,6 +297,7 @@ export type ServerChatAction =
| ChatMovableChangedAction
| ChatChangesetsChangedAction
| ChatCanvasesChangedAction
+ | ChatCanvasRegistryChangedAction
| ChatUsageAction
| ChatReasoningAction
| ChatMovedAction
@@ -519,6 +525,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool
[ActionType.ChatMovableChanged]: false,
[ActionType.ChatChangesetsChanged]: false,
[ActionType.ChatCanvasesChanged]: false,
+ [ActionType.ChatCanvasRegistryRequested]: true,
+ [ActionType.ChatCanvasRegistryChanged]: false,
[ActionType.CanvasStateChanged]: false,
[ActionType.ChatWorkingDirectorySet]: true,
[ActionType.ChatWorkingDirectoryRemoved]: true,
diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts
index 5d3337d66..030510c67 100644
--- a/types/channels-canvas/commands.ts
+++ b/types/channels-canvas/commands.ts
@@ -1,5 +1,5 @@
/**
- * Canvas Commands — discovery and explicit opening for one exact chat.
+ * Canvas Commands — explicit opening for one exact chat.
*
* Stability: 1 - Experimental
*
@@ -10,50 +10,17 @@ import type { BaseParams } from '../common/commands.js';
import type { URI } from '../common/state.js';
import type { AgentCapabilities } from '../channels-root/state.js';
import type { ChatState } from '../channels-chat/state.js';
-import type { CanvasTypeDeclaration } from './state.js';
-
-// ─── listCanvasTypes ─────────────────────────────────────────────────────────
-
-/**
- * Lists canvas types currently available to open for one exact chat.
- *
- * This read MAY lazily resume an existing persisted chat so its extension
- * registry can be reconstructed. It MUST NOT create a backing runtime for a
- * brand-new chat, send a user message, or invoke a model.
- *
- * Clients MUST NOT call this command unless the selected agent advertises
- * {@link AgentCapabilities.canvases}.
- *
- * @category Commands
- * @method listCanvasTypes
- * @direction Client → Server
- * @messageType Request
- * @version 1
- */
-export interface ListCanvasTypesParams extends BaseParams {
- /** Exact chat URI whose extension registry should be queried. */
- channel: URI;
-}
-
-/**
- * Canvas types available to open for the requested chat.
- *
- * @category Commands
- */
-export interface ListCanvasTypesResult {
- /** Available canvas types and their declaring extensions. */
- items: CanvasTypeDeclaration[];
-}
// ─── openCanvas ──────────────────────────────────────────────────────────────
/**
- * Opens or focuses one stable canvas instance in an exact chat.
+ * Opens or focuses one client-addressed canvas resource in an exact chat.
*
* The resulting live instance is published through the chat's
* {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
- * Repeating this command with the same `instanceId` focuses or refreshes that
- * logical instance rather than creating a duplicate.
+ * Repeating this command with the same `channel` focuses or refreshes that
+ * logical instance rather than creating a duplicate. Reusing the channel for
+ * a different chat, extension, or canvas type MUST fail with `Conflict`.
*
* The null result is not presentation state. Clients MUST observe
* {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -69,12 +36,15 @@ export interface ListCanvasTypesResult {
* @version 1
*/
export interface OpenCanvasParams extends BaseParams {
- /** Exact chat URI that owns the canvas instance. */
+ /**
+ * Client-chosen URI for the canvas resource, for example
+ * `ahp-canvas:/`.
+ */
channel: URI;
+ /** Exact chat URI that owns the canvas instance. */
+ chat: URI;
/** Stable identifier of the extension that declares the canvas. */
extensionId: string;
/** Provider-local canvas type identifier. */
canvasId: string;
- /** Caller-chosen stable instance identifier. */
- instanceId: string;
}
diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts
index b8dbf9057..34a7e4c47 100644
--- a/types/channels-canvas/state.ts
+++ b/types/channels-canvas/state.ts
@@ -29,7 +29,30 @@ export const enum CanvasExtensionSource {
}
/**
- * One canvas type currently available to open for an exact chat.
+ * One extension/provider that declares canvas types for an exact chat.
+ *
+ * The extension metadata is normalized here instead of repeated on every
+ * {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
+ * infer its source or management behavior by parsing it.
+ *
+ * @category Canvas State
+ */
+export interface CanvasExtensionDeclaration {
+ /** Stable opaque identifier of the extension/provider. */
+ extensionId: string;
+ /** Where the runtime discovered the extension/provider. */
+ extensionSource: CanvasExtensionSource;
+ /** Human-readable extension/provider name, when available. */
+ extensionName?: string;
+ /**
+ * Canvas types declared by this extension/provider. `canvasId` MUST be
+ * unique within this array.
+ */
+ canvases: CanvasTypeDeclaration[];
+}
+
+/**
+ * One canvas type currently available to open.
*
* This is discovery metadata, not a live canvas instance. Live instances are
* represented by {@link CanvasReference} entries on chat state and their
@@ -43,20 +66,29 @@ export const enum CanvasExtensionSource {
export interface CanvasTypeDeclaration {
/** Provider-local canvas type identifier. */
canvasId: string;
- /** Stable identifier of the extension that declares the canvas. */
- extensionId: string;
- /** Where the runtime discovered the declaring extension. */
- extensionSource: CanvasExtensionSource;
- /** Human-readable extension name, when available. */
- extensionName?: string;
/** Human-readable canvas type name. */
displayName: string;
/** Description of what the canvas type does. */
description: string;
/** Whether opening this canvas requires an input flow. */
requiresInput: boolean;
- /** Number of actions the canvas declares. */
- actionCount: number;
+}
+
+/**
+ * Canvas types currently registered for one exact chat.
+ *
+ * This is a compact presentation registry, not extension installation state
+ * and not the list of live instances. The host replaces the complete registry
+ * through `chat/canvasRegistryChanged` whenever provider registration changes.
+ *
+ * @category Canvas State
+ */
+export interface CanvasRegistry {
+ /**
+ * Declaring extensions/providers. `extensionId` MUST be unique within this
+ * array. An empty array is an authoritative loaded registry with no types.
+ */
+ extensions: CanvasExtensionDeclaration[];
}
/**
@@ -83,7 +115,13 @@ export interface CanvasReference {
* @category Canvas State
*/
export interface CanvasState {
- /** Stable caller-supplied instance identifier. */
+ /**
+ * Provider-facing instance identifier.
+ *
+ * AHP clients address this instance by the subscribed `ahp-canvas:` channel
+ * URI. They MUST NOT treat this provider identifier as a second resource
+ * identity.
+ */
instanceId: string;
/** Owning extension/provider identifier. */
extensionId: string;
diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts
index 1b7dd4583..cf6503302 100644
--- a/types/channels-chat/actions.ts
+++ b/types/channels-chat/actions.ts
@@ -6,7 +6,7 @@
import { ActionType } from '../common/actions.js';
import type { StringOrMarkdown, FileEditCollection, UsageInfo, URI } from '../common/state.js';
-import type { CanvasReference } from '../channels-canvas/state.js';
+import type { CanvasReference, CanvasRegistry } from '../channels-canvas/state.js';
import type { Changeset } from '../channels-changeset/state.js';
import type { McpAuthRequirement, SessionConfigSchema } from '../channels-session/state.js';
import type {
@@ -627,6 +627,44 @@ export interface ChatCanvasesChangedAction {
canvases: CanvasReference[] | undefined;
}
+/**
+ * Requests materialization of the exact chat's Canvas type registry.
+ *
+ * This operation is potentially effectful: for a persisted cold chat, the host
+ * may resume its backing runtime, discover extensions, and execute extension
+ * startup code. It MUST NOT create a backing for a brand-new chat, send a user
+ * message, or invoke a model.
+ *
+ * The reducer is a no-op. The host remains authoritative and publishes the
+ * complete result through
+ * {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
+ * Clients do not need to dispatch this action when
+ * {@link ChatState.canvasRegistry} is already present because live provider
+ * registration changes are published reactively.
+ *
+ * @category Chat Actions
+ * @version 1
+ * @clientDispatchable
+ */
+export interface ChatCanvasRegistryRequestedAction {
+ type: ActionType.ChatCanvasRegistryRequested;
+}
+
+/**
+ * The Canvas type registry for this exact chat changed.
+ *
+ * Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
+ * Set to `undefined` when the registry is no longer materialized or available.
+ *
+ * @category Chat Actions
+ * @version 1
+ */
+export interface ChatCanvasRegistryChangedAction {
+ type: ActionType.ChatCanvasRegistryChanged;
+ /** Authoritative registry, or `undefined` to clear it. */
+ canvasRegistry: CanvasRegistry | undefined;
+}
+
/**
* A working directory was added to this chat's
* {@link ChatState.workingDirectories} subset.
diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts
index 6da04f39e..3ba0fc371 100644
--- a/types/channels-chat/reducer.ts
+++ b/types/channels-chat/reducer.ts
@@ -489,6 +489,16 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st
: stateWithoutCanvases;
}
+ case ActionType.ChatCanvasRegistryRequested:
+ return state;
+
+ case ActionType.ChatCanvasRegistryChanged: {
+ const { canvasRegistry: _omit, ...stateWithoutCanvasRegistry } = state;
+ return action.canvasRegistry
+ ? { ...stateWithoutCanvasRegistry, canvasRegistry: action.canvasRegistry }
+ : stateWithoutCanvasRegistry;
+ }
+
// ── Working Directories ───────────────────────────────────────────────
case ActionType.ChatWorkingDirectorySet: {
diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts
index eb3cf0cbf..b9a4856b1 100644
--- a/types/channels-chat/state.ts
+++ b/types/channels-chat/state.ts
@@ -16,7 +16,7 @@ import type {
SessionStatus,
} from '../channels-session/state.js';
import type { Changeset } from '../channels-changeset/state.js';
-import type { CanvasReference } from '../channels-canvas/state.js';
+import type { CanvasReference, CanvasRegistry } from '../channels-canvas/state.js';
import type {
ContentRef,
ErrorInfo,
@@ -129,6 +129,17 @@ export interface ChatState {
* chat channel.
*/
backgroundWork?: BackgroundWork[];
+ /**
+ * Canvas types currently registered for this exact chat.
+ *
+ * Absence means the registry has not been materialized or is not currently
+ * available. Clients that need it MAY dispatch
+ * `chat/canvasRegistryRequested`; the host publishes the authoritative
+ * registry through `chat/canvasRegistryChanged`. An empty
+ * {@link CanvasRegistry.extensions} array is a loaded registry with no
+ * available canvas types.
+ */
+ canvasRegistry?: CanvasRegistry;
/**
* Live canvases currently exposed by this chat.
*
diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts
index 85fe64f6d..d4893fc14 100644
--- a/types/channels-root/state.ts
+++ b/types/channels-root/state.ts
@@ -109,12 +109,15 @@ export interface AgentCapabilities {
/**
* Experimental Canvas API.
*
- * The agent can list canvas types for an exact chat with `listCanvasTypes`
- * and open them with `openCanvas`. Live instances are published through the
- * chat's canvas references and their `ahp-canvas:` state channels.
+ * The agent can materialize an exact chat's
+ * {@link ChatState.canvasRegistry | Canvas registry} through
+ * `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
+ * Live instances are published through the chat's canvas references and
+ * their `ahp-canvas:` state channels.
*
* Clients MUST treat absence as unsupported and SHOULD tolerate additive
- * discovery metadata while the Canvas channel remains experimental.
+ * registry metadata while the Canvas channel remains experimental. This
+ * capability does not gate server-published Canvas references.
*/
canvases?: Record;
/**
diff --git a/types/common/actions.ts b/types/common/actions.ts
index 55125bf9e..f2ca54b93 100644
--- a/types/common/actions.ts
+++ b/types/common/actions.ts
@@ -71,6 +71,8 @@ import type {
ChatMovableChangedAction,
ChatChangesetsChangedAction,
ChatCanvasesChangedAction,
+ ChatCanvasRegistryRequestedAction,
+ ChatCanvasRegistryChangedAction,
ChatWorkingDirectorySetAction,
ChatWorkingDirectoryRemovedAction,
ChatUsageAction,
@@ -184,6 +186,8 @@ export const enum ActionType {
ChatMovableChanged = 'chat/movableChanged',
ChatChangesetsChanged = 'chat/changesetsChanged',
ChatCanvasesChanged = 'chat/canvasesChanged',
+ ChatCanvasRegistryRequested = 'chat/canvasRegistryRequested',
+ ChatCanvasRegistryChanged = 'chat/canvasRegistryChanged',
CanvasStateChanged = 'canvas/stateChanged',
ChatWorkingDirectorySet = 'chat/workingDirectorySet',
ChatWorkingDirectoryRemoved = 'chat/workingDirectoryRemoved',
@@ -353,6 +357,8 @@ export type StateAction =
| ChatMovableChangedAction
| ChatChangesetsChangedAction
| ChatCanvasesChangedAction
+ | ChatCanvasRegistryRequestedAction
+ | ChatCanvasRegistryChangedAction
| CanvasStateChangedAction
| ChatWorkingDirectorySetAction
| ChatWorkingDirectoryRemovedAction
diff --git a/types/common/messages.ts b/types/common/messages.ts
index c3fea651e..5f5172c0e 100644
--- a/types/common/messages.ts
+++ b/types/common/messages.ts
@@ -60,11 +60,7 @@ import type {
MoveChatResult,
DisposeChatParams,
} from '../channels-chat/commands.js';
-import type {
- ListCanvasTypesParams,
- ListCanvasTypesResult,
- OpenCanvasParams,
-} from '../channels-canvas/commands.js';
+import type { OpenCanvasParams } from '../channels-canvas/commands.js';
import type {
CreateTerminalParams,
DisposeTerminalParams,
@@ -179,7 +175,6 @@ export interface CommandMap {
'createTcpConnection': { params: CreateTcpConnectionParams; result: null };
'moveChat': { params: MoveChatParams; result: MoveChatResult };
'disposeChat': { params: DisposeChatParams; result: null };
- 'listCanvasTypes': { params: ListCanvasTypesParams; result: ListCanvasTypesResult };
'openCanvas': { params: OpenCanvasParams; result: null };
'createTerminal': { params: CreateTerminalParams; result: null };
'disposeTerminal': { params: DisposeTerminalParams; result: null };
diff --git a/types/test-cases/reducers/293-chat-canvasregistryrequested-noop.json b/types/test-cases/reducers/293-chat-canvasregistryrequested-noop.json
new file mode 100644
index 000000000..47f55868d
--- /dev/null
+++ b/types/test-cases/reducers/293-chat-canvasregistryrequested-noop.json
@@ -0,0 +1,23 @@
+{
+ "description": "chat/canvasRegistryRequested leaves state unchanged while the host materializes the registry",
+ "reducer": "chat",
+ "initial": {
+ "turns": [],
+ "resource": "ahp-chat:/test-session/main",
+ "title": "Test Chat",
+ "status": 1,
+ "modifiedAt": "1970-01-01T00:00:01.000Z"
+ },
+ "actions": [
+ {
+ "type": "chat/canvasRegistryRequested"
+ }
+ ],
+ "expected": {
+ "turns": [],
+ "resource": "ahp-chat:/test-session/main",
+ "title": "Test Chat",
+ "status": 1,
+ "modifiedAt": "1970-01-01T00:00:01.000Z"
+ }
+}
diff --git a/types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json b/types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json
new file mode 100644
index 000000000..5386b3501
--- /dev/null
+++ b/types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json
@@ -0,0 +1,57 @@
+{
+ "description": "chat/canvasRegistryChanged replaces state.canvasRegistry with the authoritative normalized registry",
+ "reducer": "chat",
+ "initial": {
+ "turns": [],
+ "resource": "ahp-chat:/test-session/main",
+ "title": "Test Chat",
+ "status": 1,
+ "modifiedAt": "1970-01-01T00:00:01.000Z"
+ },
+ "actions": [
+ {
+ "type": "chat/canvasRegistryChanged",
+ "canvasRegistry": {
+ "extensions": [
+ {
+ "extensionId": "opaque-provider-id",
+ "extensionSource": "project",
+ "extensionName": "Project Canvases",
+ "canvases": [
+ {
+ "canvasId": "diagram",
+ "displayName": "Diagram",
+ "description": "Visualize a diagram.",
+ "requiresInput": false
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "expected": {
+ "turns": [],
+ "resource": "ahp-chat:/test-session/main",
+ "title": "Test Chat",
+ "status": 1,
+ "modifiedAt": "1970-01-01T00:00:01.000Z",
+ "canvasRegistry": {
+ "extensions": [
+ {
+ "extensionId": "opaque-provider-id",
+ "extensionSource": "project",
+ "extensionName": "Project Canvases",
+ "canvases": [
+ {
+ "canvasId": "diagram",
+ "displayName": "Diagram",
+ "description": "Visualize a diagram.",
+ "requiresInput": false
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/types/test-cases/reducers/295-chat-canvasregistrychanged-clears-registry.json b/types/test-cases/reducers/295-chat-canvasregistrychanged-clears-registry.json
new file mode 100644
index 000000000..3d6826b91
--- /dev/null
+++ b/types/test-cases/reducers/295-chat-canvasregistrychanged-clears-registry.json
@@ -0,0 +1,26 @@
+{
+ "description": "chat/canvasRegistryChanged clears state.canvasRegistry when the registry is no longer materialized",
+ "reducer": "chat",
+ "initial": {
+ "turns": [],
+ "resource": "ahp-chat:/test-session/main",
+ "title": "Test Chat",
+ "status": 1,
+ "modifiedAt": "1970-01-01T00:00:01.000Z",
+ "canvasRegistry": {
+ "extensions": []
+ }
+ },
+ "actions": [
+ {
+ "type": "chat/canvasRegistryChanged"
+ }
+ ],
+ "expected": {
+ "turns": [],
+ "resource": "ahp-chat:/test-session/main",
+ "title": "Test Chat",
+ "status": 1,
+ "modifiedAt": "1970-01-01T00:00:01.000Z"
+ }
+}
diff --git a/types/test-cases/round-trips/076-chat-canvas-registry.json b/types/test-cases/round-trips/076-chat-canvas-registry.json
new file mode 100644
index 000000000..1eb04e89b
--- /dev/null
+++ b/types/test-cases/round-trips/076-chat-canvas-registry.json
@@ -0,0 +1,76 @@
+{
+ "name": "chat-canvas-registry",
+ "group": "A",
+ "description": "A chat snapshot preserves its normalized Canvas type registry separately from live Canvas references.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-chat:/peer",
+ "state": {
+ "resource": "ahp-chat:/peer",
+ "title": "Peer",
+ "status": 1,
+ "modifiedAt": "2026-10-14T10:00:00.000Z",
+ "turns": [],
+ "canvasRegistry": {
+ "extensions": [
+ {
+ "extensionId": "opaque-provider-id",
+ "extensionSource": "plugin",
+ "extensionName": "Diagram Tools",
+ "canvases": [
+ {
+ "canvasId": "diagram",
+ "displayName": "Diagram",
+ "description": "Visualize a diagram.",
+ "requiresInput": false
+ },
+ {
+ "canvasId": "form",
+ "displayName": "Form",
+ "description": "Collect structured input.",
+ "requiresInput": true
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "fromSeq": 3
+ },
+ "acceptableOutputs": [
+ {
+ "resource": "ahp-chat:/peer",
+ "state": {
+ "resource": "ahp-chat:/peer",
+ "title": "Peer",
+ "status": 1,
+ "modifiedAt": "2026-10-14T10:00:00.000Z",
+ "turns": [],
+ "canvasRegistry": {
+ "extensions": [
+ {
+ "extensionId": "opaque-provider-id",
+ "extensionSource": "plugin",
+ "extensionName": "Diagram Tools",
+ "canvases": [
+ {
+ "canvasId": "diagram",
+ "displayName": "Diagram",
+ "description": "Visualize a diagram.",
+ "requiresInput": false
+ },
+ {
+ "canvasId": "form",
+ "displayName": "Form",
+ "description": "Collect structured input.",
+ "requiresInput": true
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "fromSeq": 3
+ }
+ ]
+}
diff --git a/types/test-cases/round-trips/077-chat-canvas-registry-requested-action.json b/types/test-cases/round-trips/077-chat-canvas-registry-requested-action.json
new file mode 100644
index 000000000..fdca17e9b
--- /dev/null
+++ b/types/test-cases/round-trips/077-chat-canvas-registry-requested-action.json
@@ -0,0 +1,14 @@
+{
+ "name": "chat-canvas-registry-requested-action",
+ "group": "A",
+ "description": "chat/canvasRegistryRequested explicitly requests potentially effectful cold-registry materialization.",
+ "type": "StateAction",
+ "input": {
+ "type": "chat/canvasRegistryRequested"
+ },
+ "acceptableOutputs": [
+ {
+ "type": "chat/canvasRegistryRequested"
+ }
+ ]
+}
diff --git a/types/test-cases/round-trips/078-chat-canvas-registry-changed-action.json b/types/test-cases/round-trips/078-chat-canvas-registry-changed-action.json
new file mode 100644
index 000000000..2d0f184ad
--- /dev/null
+++ b/types/test-cases/round-trips/078-chat-canvas-registry-changed-action.json
@@ -0,0 +1,20 @@
+{
+ "name": "chat-canvas-registry-changed-action",
+ "group": "A",
+ "description": "chat/canvasRegistryChanged carries a complete normalized registry replacement.",
+ "type": "StateAction",
+ "input": {
+ "type": "chat/canvasRegistryChanged",
+ "canvasRegistry": {
+ "extensions": []
+ }
+ },
+ "acceptableOutputs": [
+ {
+ "type": "chat/canvasRegistryChanged",
+ "canvasRegistry": {
+ "extensions": []
+ }
+ }
+ ]
+}
diff --git a/types/test-cases/round-trips/079-open-canvas-client-addressed-resource.json b/types/test-cases/round-trips/079-open-canvas-client-addressed-resource.json
new file mode 100644
index 000000000..efae7eca1
--- /dev/null
+++ b/types/test-cases/round-trips/079-open-canvas-client-addressed-resource.json
@@ -0,0 +1,20 @@
+{
+ "name": "open-canvas-client-addressed-resource",
+ "group": "A",
+ "description": "openCanvas targets a client-chosen ahp-canvas resource and names its exact owning chat and declaration.",
+ "type": "OpenCanvasParams",
+ "input": {
+ "channel": "ahp-canvas:/diagram",
+ "chat": "ahp-chat:/peer",
+ "extensionId": "opaque-provider-id",
+ "canvasId": "diagram"
+ },
+ "acceptableOutputs": [
+ {
+ "channel": "ahp-canvas:/diagram",
+ "chat": "ahp-chat:/peer",
+ "extensionId": "opaque-provider-id",
+ "canvasId": "diagram"
+ }
+ ]
+}
diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts
index dea768c5f..8635b2910 100644
--- a/types/version/message-checks.ts
+++ b/types/version/message-checks.ts
@@ -60,7 +60,6 @@ type _ExpectedCommands =
| 'createTcpConnection'
| 'moveChat'
| 'disposeChat'
- | 'listCanvasTypes'
| 'openCanvas'
| 'createTerminal'
| 'disposeTerminal'
diff --git a/types/version/registry.test.ts b/types/version/registry.test.ts
index 55b8ddade..543e6c9a7 100644
--- a/types/version/registry.test.ts
+++ b/types/version/registry.test.ts
@@ -112,3 +112,20 @@ test('chat/configChanged is available starting in protocol 1.1.0', () => {
assert.equal(isActionKnownToVersion(action, '1.0.0'), false);
assert.equal(isActionKnownToVersion(action, '1.1.0'), true);
});
+
+test('chat Canvas registry actions are available starting in protocol 1.1.0', () => {
+ const requested = {
+ type: ActionType.ChatCanvasRegistryRequested,
+ } as const;
+ const changed = {
+ type: ActionType.ChatCanvasRegistryChanged,
+ canvasRegistry: { extensions: [] },
+ } as const;
+
+ assert.equal(ACTION_INTRODUCED_IN[ActionType.ChatCanvasRegistryRequested], '1.1.0');
+ assert.equal(ACTION_INTRODUCED_IN[ActionType.ChatCanvasRegistryChanged], '1.1.0');
+ assert.equal(isActionKnownToVersion(requested, '1.0.0'), false);
+ assert.equal(isActionKnownToVersion(changed, '1.0.0'), false);
+ assert.equal(isActionKnownToVersion(requested, '1.1.0'), true);
+ assert.equal(isActionKnownToVersion(changed, '1.1.0'), true);
+});
diff --git a/types/version/registry.ts b/types/version/registry.ts
index d25e68643..397744016 100644
--- a/types/version/registry.ts
+++ b/types/version/registry.ts
@@ -153,6 +153,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string
[ActionType.ChatMovableChanged]: '0.9.0',
[ActionType.ChatChangesetsChanged]: '0.9.0',
[ActionType.ChatCanvasesChanged]: '0.10.0',
+ [ActionType.ChatCanvasRegistryRequested]: '1.1.0',
+ [ActionType.ChatCanvasRegistryChanged]: '1.1.0',
[ActionType.CanvasStateChanged]: '0.10.0',
[ActionType.ChatWorkingDirectorySet]: '0.7.0',
[ActionType.ChatWorkingDirectoryRemoved]: '0.7.0',
From 200c790cd53a8d26f20d10471bb0871274f01730 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Fri, 30 Oct 2026 21:07:28 -0700
Subject: [PATCH 05/12] canvas: add URI-addressed disposal
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Generated/Commands.generated.cs | 20 ++++++++++++++++++
.../JsonSerializerContext.generated.cs | 1 +
clients/go/ahptypes/commands.generated.go | 16 ++++++++++++++
.../generated/Commands.generated.kt | 14 +++++++++++++
.../Generated/Commands.generated.swift | 21 +++++++++++++++++++
.../20261012-canvas-discovery-commands.json | 2 +-
docs/specification/chat-channel.md | 5 +++++
schema/commands.schema.json | 18 ++++++++++++++++
schema/errors.schema.json | 18 ++++++++++++++++
scripts/generate-csharp.ts | 2 +-
scripts/generate-go.ts | 2 +-
scripts/generate-kotlin.ts | 2 +-
scripts/generate-rust.ts | 2 +-
scripts/generate-swift.ts | 2 +-
types/channels-canvas/commands.ts | 20 ++++++++++++++++++
types/common/messages.ts | 3 ++-
...pose-canvas-client-addressed-resource.json | 14 +++++++++++++
types/version/message-checks.ts | 1 +
18 files changed, 156 insertions(+), 7 deletions(-)
create mode 100644 types/test-cases/round-trips/080-dispose-canvas-client-addressed-resource.json
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 6f2c0dd96..3b4218e6b 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1183,6 +1183,26 @@ public sealed record OpenCanvasParams
public required string CanvasId { get; init; }
}
+/// Closes and disposes one live canvas resource.
+///
+/// The server removes the resource from the owning chat's
+/// {@link ChatState.canvases} references and notifies the provider that owns the
+/// instance. Repeating this command after the resource is gone is a no-op.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+public sealed record DisposeCanvasParams
+{
+ /// Channel URI this command targets.
+ public required string Channel { get; init; }
+
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ [JsonPropertyName("_meta")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Meta { get; init; }
+}
+
/// Returns a list of session summaries. Used to populate session lists and sidebars.
///
/// The session list is **not** part of the state tree because it can be arbitrarily
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index 43db555f5..3dd833497 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -234,6 +234,7 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(DirectoryCustomization))]
[JsonSerializable(typeof(DirectoryEntry))]
[JsonSerializable(typeof(DispatchActionParams))]
+[JsonSerializable(typeof(DisposeCanvasParams))]
[JsonSerializable(typeof(DisposeChatParams))]
[JsonSerializable(typeof(DisposeSessionParams))]
[JsonSerializable(typeof(DisposeTerminalParams))]
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 8d1ff2986..ff4954a03 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -792,6 +792,22 @@ type OpenCanvasParams struct {
CanvasId string `json:"canvasId"`
}
+// Closes and disposes one live canvas resource.
+//
+// The server removes the resource from the owning chat's
+// {@link ChatState.canvases} references and notifies the provider that owns the
+// instance. Repeating this command after the resource is gone is a no-op.
+//
+// Clients MUST NOT call this command unless the selected agent advertises
+// {@link AgentCapabilities.canvases}.
+type DisposeCanvasParams struct {
+ // Channel URI this command targets.
+ Channel URI `json:"channel"`
+ // Optional JSON-serializable metadata associated with this request.
+ // Receivers MUST ignore keys they do not understand.
+ Meta map[string]json.RawMessage `json:"_meta,omitempty"`
+}
+
// Returns a list of session summaries. Used to populate session lists and sidebars.
//
// The session list is **not** part of the state tree because it can be arbitrarily
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index 24cd611b5..d65579687 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1050,6 +1050,20 @@ data class OpenCanvasParams(
val canvasId: String
)
+@Serializable
+data class DisposeCanvasParams(
+ /**
+ * Channel URI this command targets.
+ */
+ val channel: String,
+ /**
+ * Optional JSON-serializable metadata associated with this request.
+ * Receivers MUST ignore keys they do not understand.
+ */
+ @SerialName("_meta")
+ val meta: Map? = null
+)
+
@Serializable
data class ListSessionsParams(
/**
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index e92aa552f..95125a7c3 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1190,6 +1190,27 @@ public struct OpenCanvasParams: Codable, Sendable {
}
}
+public struct DisposeCanvasParams: Codable, Sendable {
+ /// Channel URI this command targets.
+ public var channel: String
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ public var meta: [String: AnyCodable]?
+
+ enum CodingKeys: String, CodingKey {
+ case channel
+ case meta = "_meta"
+ }
+
+ public init(
+ channel: String,
+ meta: [String: AnyCodable]? = nil
+ ) {
+ self.channel = channel
+ self.meta = meta
+ }
+}
+
public struct ListSessionsParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
diff --git a/docs/.changes/20261012-canvas-discovery-commands.json b/docs/.changes/20261012-canvas-discovery-commands.json
index 6c90f1307..4eb9763a2 100644
--- a/docs/.changes/20261012-canvas-discovery-commands.json
+++ b/docs/.changes/20261012-canvas-discovery-commands.json
@@ -1,4 +1,4 @@
{
"type": "added",
- "message": "Experimental exact-chat Canvas registry state, explicit cold-registry materialization, and URI-addressed `openCanvas` support."
+ "message": "Experimental exact-chat Canvas registry state, explicit cold-registry materialization, and URI-addressed `openCanvas` and `disposeCanvas` support."
}
diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md
index 1c8c07f25..b304dd7da 100644
--- a/docs/specification/chat-channel.md
+++ b/docs/specification/chat-channel.md
@@ -124,6 +124,11 @@ To open a no-input declaration, the client sends
that logical instance. Live membership and presentation then flow through
`ChatState.canvases` and the Canvas channel.
+To close a live instance, the client sends
+[`disposeCanvas`](/reference/canvas#disposecanvas) with that instance's
+`ahp-canvas:` URI. The host removes the chat membership and disposes the
+provider-facing instance; disposing an already-removed resource is a no-op.
+
### Per-chat configuration
[`ChatState.config`](/reference/chat#chatstate) holds the chat's own values for
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index be3f70ffa..9dd30d634 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -1609,6 +1609,24 @@
"canvasId"
]
},
+ "DisposeCanvasParams": {
+ "type": "object",
+ "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references and notifies the provider that owns the\ninstance. Repeating this command after the resource is gone is a no-op.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "properties": {
+ "channel": {
+ "$ref": "#/$defs/URI",
+ "description": "Channel URI this command targets."
+ },
+ "_meta": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
+ }
+ },
+ "required": [
+ "channel"
+ ]
+ },
"CreateTerminalParams": {
"type": "object",
"description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.",
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 29926fc41..820682ecb 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -7832,6 +7832,24 @@
"canvasId"
]
},
+ "DisposeCanvasParams": {
+ "type": "object",
+ "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references and notifies the provider that owns the\ninstance. Repeating this command after the resource is gone is a no-op.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "properties": {
+ "channel": {
+ "$ref": "#/$defs/URI",
+ "description": "Channel URI this command targets."
+ },
+ "_meta": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand."
+ }
+ },
+ "required": [
+ "channel"
+ ]
+ },
"CreateTerminalParams": {
"type": "object",
"description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.",
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index 2c1d59fd6..4fba24edc 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -2338,7 +2338,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str
{ name: 'CreateChatParams' },
{ name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination' }, { name: 'ChatMoveToNewSessionDestination' }, { name: 'MoveChatParams' }, { name: 'MoveChatResult' },
- { name: 'OpenCanvasParams' },
+ { name: 'OpenCanvasParams' }, { name: 'DisposeCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index 1748df426..073ad0238 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -1822,7 +1822,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str
{ name: 'DisposeSessionParams' },
{ name: 'ForkChatSource' }, { name: 'SideChatSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination' }, { name: 'ChatMoveToNewSessionDestination' }, { name: 'MoveChatParams' }, { name: 'MoveChatResult' },
- { name: 'OpenCanvasParams' },
+ { name: 'OpenCanvasParams' }, { name: 'DisposeCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index 3917bbcf3..cba037bd9 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -1800,7 +1800,7 @@ const COMMAND_STRUCTS = [
'CreateSessionParams', 'DisposeSessionParams',
'CreateChatParams', 'DisposeChatParams',
'ChatMoveToSessionDestination', 'ChatMoveToNewSessionDestination', 'MoveChatParams', 'MoveChatResult',
- 'OpenCanvasParams',
+ 'OpenCanvasParams', 'DisposeCanvasParams',
'ListSessionsParams', 'ListSessionsResult',
'ResourceReadParams', 'ResourceReadResult',
'ResourceWriteParams', 'ResourceWriteResult',
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index 1f4d4f479..c4d3dfdb2 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -1869,7 +1869,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s
{ name: 'DisposeChatParams' },
{ name: 'ChatMoveToSessionDestination', omitDiscriminants: true }, { name: 'ChatMoveToNewSessionDestination', omitDiscriminants: true },
{ name: 'MoveChatParams' }, { name: 'MoveChatResult' },
- { name: 'OpenCanvasParams' },
+ { name: 'OpenCanvasParams' }, { name: 'DisposeCanvasParams' },
{ name: 'ListSessionsParams' }, { name: 'ListSessionsResult' },
{ name: 'ResourceReadParams' }, { name: 'ResourceReadResult' },
{ name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' },
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index 444eb05f2..db1224f01 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -1722,7 +1722,7 @@ const COMMAND_STRUCTS = [
'CreateSessionParams', 'DisposeSessionParams',
'CreateChatParams', 'DisposeChatParams',
'ChatMoveToSessionDestination', 'ChatMoveToNewSessionDestination', 'MoveChatParams', 'MoveChatResult',
- 'OpenCanvasParams',
+ 'OpenCanvasParams', 'DisposeCanvasParams',
'ListSessionsParams', 'ListSessionsResult',
'ResourceReadParams', 'ResourceReadResult',
'ResourceWriteParams', 'ResourceWriteResult',
diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts
index 030510c67..89aaa3dc8 100644
--- a/types/channels-canvas/commands.ts
+++ b/types/channels-canvas/commands.ts
@@ -48,3 +48,23 @@ export interface OpenCanvasParams extends BaseParams {
/** Provider-local canvas type identifier. */
canvasId: string;
}
+
+// ─── disposeCanvas ───────────────────────────────────────────────────────────
+
+/**
+ * Closes and disposes one live canvas resource.
+ *
+ * The server removes the resource from the owning chat's
+ * {@link ChatState.canvases} references and notifies the provider that owns the
+ * instance. Repeating this command after the resource is gone is a no-op.
+ *
+ * Clients MUST NOT call this command unless the selected agent advertises
+ * {@link AgentCapabilities.canvases}.
+ *
+ * @category Commands
+ * @method disposeCanvas
+ * @direction Client → Server
+ * @messageType Request
+ * @version 1
+ */
+export interface DisposeCanvasParams extends BaseParams {}
diff --git a/types/common/messages.ts b/types/common/messages.ts
index 5f5172c0e..a6d033969 100644
--- a/types/common/messages.ts
+++ b/types/common/messages.ts
@@ -60,7 +60,7 @@ import type {
MoveChatResult,
DisposeChatParams,
} from '../channels-chat/commands.js';
-import type { OpenCanvasParams } from '../channels-canvas/commands.js';
+import type { DisposeCanvasParams, OpenCanvasParams } from '../channels-canvas/commands.js';
import type {
CreateTerminalParams,
DisposeTerminalParams,
@@ -176,6 +176,7 @@ export interface CommandMap {
'moveChat': { params: MoveChatParams; result: MoveChatResult };
'disposeChat': { params: DisposeChatParams; result: null };
'openCanvas': { params: OpenCanvasParams; result: null };
+ 'disposeCanvas': { params: DisposeCanvasParams; result: null };
'createTerminal': { params: CreateTerminalParams; result: null };
'disposeTerminal': { params: DisposeTerminalParams; result: null };
'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult };
diff --git a/types/test-cases/round-trips/080-dispose-canvas-client-addressed-resource.json b/types/test-cases/round-trips/080-dispose-canvas-client-addressed-resource.json
new file mode 100644
index 000000000..a26fcd528
--- /dev/null
+++ b/types/test-cases/round-trips/080-dispose-canvas-client-addressed-resource.json
@@ -0,0 +1,14 @@
+{
+ "name": "dispose-canvas-client-addressed-resource",
+ "group": "A",
+ "description": "disposeCanvas targets the client-chosen ahp-canvas resource identity.",
+ "type": "DisposeCanvasParams",
+ "input": {
+ "channel": "ahp-canvas:/diagram"
+ },
+ "acceptableOutputs": [
+ {
+ "channel": "ahp-canvas:/diagram"
+ }
+ ]
+}
diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts
index 8635b2910..6a7d63404 100644
--- a/types/version/message-checks.ts
+++ b/types/version/message-checks.ts
@@ -61,6 +61,7 @@ type _ExpectedCommands =
| 'moveChat'
| 'disposeChat'
| 'openCanvas'
+ | 'disposeCanvas'
| 'createTerminal'
| 'disposeTerminal'
| 'createResourceWatch'
From a3b43aa95d03eaee7164a4911d3e7517275c1514 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Fri, 30 Oct 2026 21:13:58 -0700
Subject: [PATCH 06/12] canvas: regenerate Rust disposal type
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
clients/rust/crates/ahp-types/src/commands.rs | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 913e5bb86..56f2feb2d 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -1086,6 +1086,25 @@ pub struct OpenCanvasParams {
pub canvas_id: String,
}
+/// Closes and disposes one live canvas resource.
+///
+/// The server removes the resource from the owning chat's
+/// {@link ChatState.canvases} references and notifies the provider that owns the
+/// instance. Repeating this command after the resource is gone is a no-op.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DisposeCanvasParams {
+ /// Channel URI this command targets.
+ pub channel: Uri,
+ /// Optional JSON-serializable metadata associated with this request.
+ /// Receivers MUST ignore keys they do not understand.
+ #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
+ pub meta: Option,
+}
+
/// Returns a list of session summaries. Used to populate session lists and sidebars.
///
/// The session list is **not** part of the state tree because it can be arbitrarily
From b4c0dd9a46f4939f95ff407d621c76061dcbdf63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Fri, 30 Oct 2026 21:17:30 -0700
Subject: [PATCH 07/12] test: cover Canvas disposal round trips
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs | 2 ++
clients/go/ahptypes/roundtrip_fixture_test.go | 4 ++++
.../com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt | 2 ++
clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs | 5 +++--
.../TypesRoundTripFixtureTests.swift | 2 ++
clients/typescript/test/types-round-trip.test.ts | 3 ++-
6 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
index b9431b84f..63f351102 100644
--- a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
+++ b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
@@ -194,6 +194,8 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type,
return Wrap(Ser.Deserialize(inputJson));
case "OpenCanvasParams":
return Wrap(Ser.Deserialize(inputJson));
+ case "DisposeCanvasParams":
+ return Wrap(Ser.Deserialize(inputJson));
default:
throw new Xunit.Sdk.XunitException(
$"round-trip fixture: unknown wire type \"{type}\". "
diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go
index 115d8cf7b..d5b923f73 100644
--- a/clients/go/ahptypes/roundtrip_fixture_test.go
+++ b/clients/go/ahptypes/roundtrip_fixture_test.go
@@ -320,6 +320,10 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string {
var v OpenCanvasParams
dec(&v)
return enc(&v)
+ case "DisposeCanvasParams":
+ var v DisposeCanvasParams
+ dec(&v)
+ return enc(&v)
default:
t.Fatalf("%s: round-trip fixture: unknown wire type %q. Add a decode entry to decodeAndReencode.", name, typ)
return ""
diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
index 6a8a83986..8da55806c 100644
--- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
+++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
@@ -48,6 +48,7 @@ import com.microsoft.agenthostprotocol.generated.CreateTcpConnectionParams
import com.microsoft.agenthostprotocol.generated.ResourceReadParams
import com.microsoft.agenthostprotocol.generated.ResourceReadResult
import com.microsoft.agenthostprotocol.generated.OpenCanvasParams
+import com.microsoft.agenthostprotocol.generated.DisposeCanvasParams
import com.microsoft.agenthostprotocol.generated.JsonRpcErrorResponse
import com.microsoft.agenthostprotocol.generated.JsonRpcNotification
import com.microsoft.agenthostprotocol.generated.JsonRpcRequest
@@ -283,6 +284,7 @@ class RoundTripCorpusTest {
"ResourceReadParams" -> rt(ResourceReadParams.serializer())
"ResourceReadResult" -> rt(ResourceReadResult.serializer())
"OpenCanvasParams" -> rt(OpenCanvasParams.serializer())
+ "DisposeCanvasParams" -> rt(DisposeCanvasParams.serializer())
else -> fail(
"$file: unknown wire type \"$typeName\". " +
"Add a decode entry to decodeAndReencode.",
diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
index 0027952e5..a60e2b17a 100644
--- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
+++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
@@ -30,8 +30,8 @@ use ahp_types::{
actions::{ActionEnvelope, StateAction},
commands::{
ChangesetOperationTarget, ChatSource, CreateChatParams, CreateTcpConnectionParams,
- Implementation, InitializeResult, ReconnectParams, ReconnectResult, ResourceReadParams,
- OpenCanvasParams, ResourceReadResult, SubscribeParams, SubscribeResult,
+ DisposeCanvasParams, Implementation, InitializeResult, OpenCanvasParams, ReconnectParams,
+ ReconnectResult, ResourceReadParams, ResourceReadResult, SubscribeParams, SubscribeResult,
},
common::StringOrMarkdown,
messages::JsonRpcMessage,
@@ -246,6 +246,7 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result<
"ResourceReadParams" => round_trip!(ResourceReadParams),
"ResourceReadResult" => round_trip!(ResourceReadResult),
"OpenCanvasParams" => round_trip!(OpenCanvasParams),
+ "DisposeCanvasParams" => round_trip!(DisposeCanvasParams),
other => Err(format!(
"{}: unknown wire type {:?}. Add a decode entry to decode_and_reencode.",
file, other
diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
index 6b2cc2cd4..617ed5139 100644
--- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
+++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
@@ -220,6 +220,8 @@ final class TypesRoundTripFixtureTests: XCTestCase {
return try reencode(dec.decode(ResourceReadResult.self, from: inputData))
case "OpenCanvasParams":
return try reencode(dec.decode(OpenCanvasParams.self, from: inputData))
+ case "DisposeCanvasParams":
+ return try reencode(dec.decode(DisposeCanvasParams.self, from: inputData))
default:
throw FixtureError.message(
"round-trip fixture: unknown wire type \"\(type)\". Add a decode entry to decodeAndReencode.")
diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts
index 75e7a264b..7ad313f9d 100644
--- a/clients/typescript/test/types-round-trip.test.ts
+++ b/clients/typescript/test/types-round-trip.test.ts
@@ -67,7 +67,7 @@ import type { ChannelFrameParams, ChannelCreditParams } from '../src/types/commo
import type { TcpDataParams, TcpEofParams, TcpResetParams } from '../src/types/channels-tcp/notifications.js';
import type { CreateTcpConnectionParams } from '../src/types/channels-tcp/commands.js';
import type { ChatSource, CreateChatParams } from '../src/types/channels-chat/commands.js';
-import type { OpenCanvasParams } from '../src/types/channels-canvas/commands.js';
+import type { DisposeCanvasParams, OpenCanvasParams } from '../src/types/channels-canvas/commands.js';
// ─── Fixture directory ───────────────────────────────────────────────────────
@@ -266,6 +266,7 @@ function bindToType(file: string, type: string, parsed: unknown): void {
case 'ResourceReadParams': void (parsed as ResourceReadParams); break;
case 'ResourceReadResult': void (parsed as ResourceReadResult); break;
case 'OpenCanvasParams': void (parsed as OpenCanvasParams); break;
+ case 'DisposeCanvasParams': void (parsed as DisposeCanvasParams); break;
default:
throw new Error(
`${file}: unknown wire type "${type}". Add a decode entry to bindToType.`,
From 0563c53c4785ef64dadc705eb97ff869735e68ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Fri, 30 Oct 2026 22:28:13 -0700
Subject: [PATCH 08/12] canvas: harden registry and lifecycle protocol
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Generated/Commands.generated.cs | 19 ++++++++--
.../JsonSerializerContext.generated.cs | 1 +
.../Generated/State.generated.cs | 22 +++++++++--
.../dotnet/src/AgentHostProtocol/Reducers.cs | 1 +
clients/go/ahptypes/canvas_capability_test.go | 23 ++++++++++++
clients/go/ahptypes/commands.generated.go | 11 ++++++
clients/go/ahptypes/state.generated.go | 17 ++++++++-
.../generated/Actions.generated.kt | 15 ++++++++
.../generated/Commands.generated.kt | 37 +++++++++++++++++++
.../generated/State.generated.kt | 19 +++++++++-
clients/rust/crates/ahp-types/src/commands.rs | 11 ++++++
clients/rust/crates/ahp-types/src/state.rs | 18 ++++++++-
.../Generated/Actions.generated.swift | 11 ++++++
.../Generated/Commands.generated.swift | 33 +++++++++++++++++
.../Generated/State.generated.swift | 24 ++++++++++--
schema/notifications.schema.json | 14 ++++---
schema/state.schema.json | 14 ++++---
scripts/generate-csharp.ts | 1 +
scripts/generate-go.ts | 1 +
scripts/generate-kotlin.ts | 1 +
scripts/generate-markdown.ts | 6 +++
scripts/generate-rust.ts | 1 +
scripts/generate-swift.ts | 1 +
scripts/read-stability.test.ts | 2 +-
types/channels-canvas/commands.ts | 13 ++++++-
types/channels-canvas/state.ts | 6 ++-
types/channels-chat/actions.ts | 2 +
types/channels-chat/state.ts | 1 +
types/channels-root/state.ts | 12 +++++-
29 files changed, 305 insertions(+), 32 deletions(-)
create mode 100644 clients/go/ahptypes/canvas_capability_test.go
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 3b4218e6b..e817d7f74 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1154,17 +1154,24 @@ public sealed record MoveChatResult
/// Repeating this command with the same `channel` focuses or refreshes that
/// logical instance rather than creating a duplicate. Reusing the channel for
/// a different chat, extension, or canvas type MUST fail with `Conflict`.
+/// Canvas channels use the canonical form
+/// `ahp-canvas:/<segment>[/<segment>...]`: the lowercase scheme, no authority,
+/// query, or fragment, and one or more non-empty path segments containing only
+/// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
+/// non-canonical channels so URI parsing cannot change the resource identity.
///
/// The null result is not presentation state. Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
/// authoritative membership, availability, and presentation state.
///
/// Clients MUST NOT call this command unless the selected agent advertises
-/// {@link AgentCapabilities.canvases}.
+/// {@link AgentCapabilities.canvases}.
+///
+/// Stability: 1.0 - Early development.
public sealed record OpenCanvasParams
{
- /// Client-chosen URI for the canvas resource, for example
- /// `ahp-canvas:/<uuid>`.
+ /// Client-chosen canonical URI for the canvas resource, for example
+ /// `ahp-canvas:/<uuid>`. See the command-level canonical-form requirement.
public required string Channel { get; init; }
/// Optional JSON-serializable metadata associated with this request.
@@ -1188,9 +1195,13 @@ public sealed record OpenCanvasParams
/// The server removes the resource from the owning chat's
/// {@link ChatState.canvases} references and notifies the provider that owns the
/// instance. Repeating this command after the resource is gone is a no-op.
+/// The inherited `channel` MUST use the canonical Canvas URI form documented by
+/// {@link OpenCanvasParams}.
///
/// Clients MUST NOT call this command unless the selected agent advertises
-/// {@link AgentCapabilities.canvases}.
+/// {@link AgentCapabilities.canvases}.
+///
+/// Stability: 1.0 - Early development.
public sealed record DisposeCanvasParams
{
/// Channel URI this command targets.
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index 3dd833497..c850001c3 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -83,6 +83,7 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(BackgroundSubagentWork))]
[JsonSerializable(typeof(BackgroundWork))]
[JsonSerializable(typeof(BackgroundWorkKind))]
+[JsonSerializable(typeof(CanvasesCapability))]
[JsonSerializable(typeof(CanvasExtensionDeclaration))]
[JsonSerializable(typeof(CanvasExtensionSource))]
[JsonSerializable(typeof(CanvasReference))]
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index f85143041..50b177480 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -2125,9 +2125,11 @@ public sealed record AgentCapabilities
///
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
/// registry metadata while the Canvas channel remains experimental. This
- /// capability does not gate server-published Canvas references.
+ /// capability does not gate server-published Canvas references.
+ ///
+ /// Stability: 1.0 - Early development.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public Dictionary? Canvases { get; init; }
+ public CanvasesCapability? Canvases { get; init; }
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -2150,6 +2152,14 @@ public sealed record AgentCapabilities
public MultipleWorkingDirectoriesCapability? MultipleWorkingDirectories { get; init; }
}
+/// Presence capability for the experimental Canvas registry and lifecycle
+/// commands.
+///
+/// Stability: 1.0 - Early development.
+public sealed record CanvasesCapability
+{
+}
+
/// Options for the {@link AgentCapabilities.multipleChats} capability.
public sealed record MultipleChatsCapability
{
@@ -2628,7 +2638,9 @@ public sealed class ChatState
/// `chat/canvasRegistryRequested`; the host publishes the authoritative
/// registry through `chat/canvasRegistryChanged`. An empty
/// {@link CanvasRegistry.extensions} array is a loaded registry with no
- /// available canvas types.
+ /// available canvas types.
+ ///
+ /// Stability: 1.0 - Early development.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CanvasRegistry? CanvasRegistry { get; set; }
@@ -2757,7 +2769,9 @@ public sealed record CanvasRegistry
/// channel shape.
public sealed record CanvasReference
{
- /// Canvas channel URI. Subscribe to this resource for the full state.
+ /// Canonical Canvas channel URI. Subscribe to this resource for the full
+ /// state. It uses the `ahp-canvas:/<segment>[/<segment>...]` form documented
+ /// by `openCanvas`.
public required string Resource { get; init; }
}
diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs
index a0eae101f..d0f5c8869 100644
--- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs
+++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs
@@ -2645,6 +2645,7 @@ public static ReduceOutcome ApplyToAutomationRun(
"chat/isReadChanged",
"chat/isArchivedChanged",
"chat/configChanged",
+ "chat/canvasRegistryRequested",
"chat/inputAnswerChanged",
"chat/inputCompleted",
"chat/truncated",
diff --git a/clients/go/ahptypes/canvas_capability_test.go b/clients/go/ahptypes/canvas_capability_test.go
new file mode 100644
index 000000000..94b55bd3f
--- /dev/null
+++ b/clients/go/ahptypes/canvas_capability_test.go
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package ahptypes
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestCanvasCapabilityPresenceRoundTrip(t *testing.T) {
+ var capabilities AgentCapabilities
+ if err := json.Unmarshal([]byte(`{"canvases":{}}`), &capabilities); err != nil {
+ t.Fatal(err)
+ }
+ encoded, err := json.Marshal(capabilities)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(encoded) != `{"canvases":{}}` {
+ t.Fatalf("canvas capability presence was not preserved: %s", encoded)
+ }
+}
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index ff4954a03..83403062b 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -771,6 +771,11 @@ type MoveChatResult struct {
// Repeating this command with the same `channel` focuses or refreshes that
// logical instance rather than creating a duplicate. Reusing the channel for
// a different chat, extension, or canvas type MUST fail with `Conflict`.
+// Canvas channels use the canonical form
+// `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
+// query, or fragment, and one or more non-empty path segments containing only
+// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
+// non-canonical channels so URI parsing cannot change the resource identity.
//
// The null result is not presentation state. Clients MUST observe
// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -778,6 +783,8 @@ type MoveChatResult struct {
//
// Clients MUST NOT call this command unless the selected agent advertises
// {@link AgentCapabilities.canvases}.
+//
+// Stability: 1.0 - Early development.
type OpenCanvasParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
@@ -797,9 +804,13 @@ type OpenCanvasParams struct {
// The server removes the resource from the owning chat's
// {@link ChatState.canvases} references and notifies the provider that owns the
// instance. Repeating this command after the resource is gone is a no-op.
+// The inherited `channel` MUST use the canonical Canvas URI form documented by
+// {@link OpenCanvasParams}.
//
// Clients MUST NOT call this command unless the selected agent advertises
// {@link AgentCapabilities.canvases}.
+//
+// Stability: 1.0 - Early development.
type DisposeCanvasParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index 8255f247c..ec4b5518d 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -813,7 +813,9 @@ type AgentCapabilities struct {
// Clients MUST treat absence as unsupported and SHOULD tolerate additive
// registry metadata while the Canvas channel remains experimental. This
// capability does not gate server-published Canvas references.
- Canvases map[string]json.RawMessage `json:"canvases,omitempty"`
+ //
+ // Stability: 1.0 - Early development.
+ Canvases *CanvasesCapability `json:"canvases,omitempty"`
// The agent can host more than one concurrent chat per session. When absent,
// clients MUST NOT call `createChat` to open chats beyond the default one the
// session starts with. An empty object `{}` advertises multi-chat without
@@ -832,6 +834,13 @@ type AgentCapabilities struct {
MultipleWorkingDirectories *MultipleWorkingDirectoriesCapability `json:"multipleWorkingDirectories,omitempty"`
}
+// Presence capability for the experimental Canvas registry and lifecycle
+// commands.
+//
+// Stability: 1.0 - Early development.
+type CanvasesCapability struct {
+}
+
// Options for the {@link AgentCapabilities.multipleChats} capability.
type MultipleChatsCapability struct {
// The agent can fork a chat from a specific turn. When absent or `false`,
@@ -1472,6 +1481,8 @@ type ChatState struct {
// registry through `chat/canvasRegistryChanged`. An empty
// {@link CanvasRegistry.extensions} array is a loaded registry with no
// available canvas types.
+ //
+ // Stability: 1.0 - Early development.
CanvasRegistry *CanvasRegistry `json:"canvasRegistry,omitempty"`
// Live canvases currently exposed by this chat.
//
@@ -1570,7 +1581,9 @@ type CanvasRegistry struct {
// canvas presentation model can evolve without changing the stable chat
// channel shape.
type CanvasReference struct {
- // Canvas channel URI. Subscribe to this resource for the full state.
+ // Canonical Canvas channel URI. Subscribe to this resource for the full
+ // state. It uses the `ahp-canvas:/[/...]` form documented
+ // by `openCanvas`.
Resource URI `json:"resource"`
}
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
index df53faf52..9818b20b0 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
@@ -258,6 +258,13 @@ data class SessionDefaultChatChangedAction(
val defaultChat: String? = null
)
+/**
+ * A new message has been sent to the agent, and a new turn starts.
+ *
+ * A client is only allowed to send {@link MessageKind.User} messages.
+ *
+ * Stability: 1.0 - Early development.
+ */
@Serializable
data class ChatTurnStartedAction(
val type: ActionType,
@@ -291,6 +298,14 @@ data class ChatTurnStartedAction(
val meta: Map? = null
)
+/**
+ * Streaming text chunk from the assistant, appended to a specific response part.
+ *
+ * The server MUST first emit a `chat/responsePart` to create the target
+ * markdown part, then use this action to append text to it.
+ *
+ * Stability: 1.0 - Early development.
+ */
@Serializable
data class ChatDeltaAction(
val type: ActionType,
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index d65579687..bd6233845 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1024,6 +1024,29 @@ data class MoveChatResult(
val session: String
)
+/**
+ * Opens or focuses one client-addressed canvas resource in an exact chat.
+ *
+ * The resulting live instance is published through the chat's
+ * {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
+ * Repeating this command with the same `channel` focuses or refreshes that
+ * logical instance rather than creating a duplicate. Reusing the channel for
+ * a different chat, extension, or canvas type MUST fail with `Conflict`.
+ * Canvas channels use the canonical form
+ * `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
+ * query, or fragment, and one or more non-empty path segments containing only
+ * URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
+ * non-canonical channels so URI parsing cannot change the resource identity.
+ *
+ * The null result is not presentation state. Clients MUST observe
+ * {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
+ * authoritative membership, availability, and presentation state.
+ *
+ * Clients MUST NOT call this command unless the selected agent advertises
+ * {@link AgentCapabilities.canvases}.
+ *
+ * Stability: 1.0 - Early development.
+ */
@Serializable
data class OpenCanvasParams(
/**
@@ -1050,6 +1073,20 @@ data class OpenCanvasParams(
val canvasId: String
)
+/**
+ * Closes and disposes one live canvas resource.
+ *
+ * The server removes the resource from the owning chat's
+ * {@link ChatState.canvases} references and notifies the provider that owns the
+ * instance. Repeating this command after the resource is gone is a no-op.
+ * The inherited `channel` MUST use the canonical Canvas URI form documented by
+ * {@link OpenCanvasParams}.
+ *
+ * Clients MUST NOT call this command unless the selected agent advertises
+ * {@link AgentCapabilities.canvases}.
+ *
+ * Stability: 1.0 - Early development.
+ */
@Serializable
data class DisposeCanvasParams(
/**
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 6030c6c91..0e854032b 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1559,8 +1559,10 @@ data class AgentCapabilities(
* Clients MUST treat absence as unsupported and SHOULD tolerate additive
* registry metadata while the Canvas channel remains experimental. This
* capability does not gate server-published Canvas references.
+ *
+ * Stability: 1.0 - Early development.
*/
- val canvases: Map? = null,
+ val canvases: CanvasesCapability? = null,
/**
* The agent can host more than one concurrent chat per session. When absent,
* clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -1583,6 +1585,15 @@ data class AgentCapabilities(
val multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = null
)
+/**
+ * Presence capability for the experimental Canvas registry and lifecycle
+ * commands.
+ *
+ * Stability: 1.0 - Early development.
+ */
+@Serializable
+class CanvasesCapability
+
@Serializable
data class MultipleChatsCapability(
/**
@@ -1921,6 +1932,8 @@ data class ChatState(
* registry through `chat/canvasRegistryChanged`. An empty
* {@link CanvasRegistry.extensions} array is a loaded registry with no
* available canvas types.
+ *
+ * Stability: 1.0 - Early development.
*/
val canvasRegistry: CanvasRegistry? = null,
/**
@@ -2359,7 +2372,9 @@ data class CanvasRegistry(
@Serializable
data class CanvasReference(
/**
- * Canvas channel URI. Subscribe to this resource for the full state.
+ * Canonical Canvas channel URI. Subscribe to this resource for the full
+ * state. It uses the `ahp-canvas:/[/...]` form documented
+ * by `openCanvas`.
*/
val resource: String
)
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 56f2feb2d..d946fc3f6 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -1062,6 +1062,11 @@ pub struct MoveChatResult {
/// Repeating this command with the same `channel` focuses or refreshes that
/// logical instance rather than creating a duplicate. Reusing the channel for
/// a different chat, extension, or canvas type MUST fail with `Conflict`.
+/// Canvas channels use the canonical form
+/// `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
+/// query, or fragment, and one or more non-empty path segments containing only
+/// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
+/// non-canonical channels so URI parsing cannot change the resource identity.
///
/// The null result is not presentation state. Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -1069,6 +1074,8 @@ pub struct MoveChatResult {
///
/// Clients MUST NOT call this command unless the selected agent advertises
/// {@link AgentCapabilities.canvases}.
+///
+/// Stability: 1.0 - Early development.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenCanvasParams {
@@ -1091,9 +1098,13 @@ pub struct OpenCanvasParams {
/// The server removes the resource from the owning chat's
/// {@link ChatState.canvases} references and notifies the provider that owns the
/// instance. Repeating this command after the resource is gone is a no-op.
+/// The inherited `channel` MUST use the canonical Canvas URI form documented by
+/// {@link OpenCanvasParams}.
///
/// Clients MUST NOT call this command unless the selected agent advertises
/// {@link AgentCapabilities.canvases}.
+///
+/// Stability: 1.0 - Early development.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisposeCanvasParams {
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index 61768d190..6f33aaa99 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1834,8 +1834,10 @@ pub struct AgentCapabilities {
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
/// registry metadata while the Canvas channel remains experimental. This
/// capability does not gate server-published Canvas references.
+ ///
+ /// Stability: 1.0 - Early development.
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub canvases: Option,
+ pub canvases: Option,
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
/// session starts with. An empty object `{}` advertises multi-chat without
@@ -1856,6 +1858,14 @@ pub struct AgentCapabilities {
pub multiple_working_directories: Option,
}
+/// Presence capability for the experimental Canvas registry and lifecycle
+/// commands.
+///
+/// Stability: 1.0 - Early development.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CanvasesCapability {}
+
/// Options for the {@link AgentCapabilities.multipleChats} capability.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
@@ -2177,6 +2187,8 @@ pub struct ChatState {
/// registry through `chat/canvasRegistryChanged`. An empty
/// {@link CanvasRegistry.extensions} array is a loaded registry with no
/// available canvas types.
+ ///
+ /// Stability: 1.0 - Early development.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canvas_registry: Option,
/// Live canvases currently exposed by this chat.
@@ -2293,7 +2305,9 @@ pub struct CanvasRegistry {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasReference {
- /// Canvas channel URI. Subscribe to this resource for the full state.
+ /// Canonical Canvas channel URI. Subscribe to this resource for the full
+ /// state. It uses the `ahp-canvas:/[/...]` form documented
+ /// by `openCanvas`.
pub resource: Uri,
}
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
index 3a7d7a01a..724479b71 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
@@ -524,6 +524,11 @@ public struct SessionDefaultChatChangedAction: Codable, Sendable {
}
}
+/// A new message has been sent to the agent, and a new turn starts.
+///
+/// A client is only allowed to send {@link MessageKind.User} messages.
+///
+/// Stability: 1.0 - Early development.
public struct ChatTurnStartedAction: Codable, Sendable {
public var type: ActionType
/// Turn identifier
@@ -570,6 +575,12 @@ public struct ChatTurnStartedAction: Codable, Sendable {
}
}
+/// Streaming text chunk from the assistant, appended to a specific response part.
+///
+/// The server MUST first emit a `chat/responsePart` to create the target
+/// markdown part, then use this action to append text to it.
+///
+/// Stability: 1.0 - Early development.
public struct ChatDeltaAction: Codable, Sendable {
public var type: ActionType
/// Turn identifier
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index 95125a7c3..90628d3ac 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1154,6 +1154,27 @@ public struct MoveChatResult: Codable, Sendable {
}
}
+/// Opens or focuses one client-addressed canvas resource in an exact chat.
+///
+/// The resulting live instance is published through the chat's
+/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
+/// Repeating this command with the same `channel` focuses or refreshes that
+/// logical instance rather than creating a duplicate. Reusing the channel for
+/// a different chat, extension, or canvas type MUST fail with `Conflict`.
+/// Canvas channels use the canonical form
+/// `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
+/// query, or fragment, and one or more non-empty path segments containing only
+/// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
+/// non-canonical channels so URI parsing cannot change the resource identity.
+///
+/// The null result is not presentation state. Clients MUST observe
+/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
+/// authoritative membership, availability, and presentation state.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+///
+/// Stability: 1.0 - Early development.
public struct OpenCanvasParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
@@ -1190,6 +1211,18 @@ public struct OpenCanvasParams: Codable, Sendable {
}
}
+/// Closes and disposes one live canvas resource.
+///
+/// The server removes the resource from the owning chat's
+/// {@link ChatState.canvases} references and notifies the provider that owns the
+/// instance. Repeating this command after the resource is gone is a no-op.
+/// The inherited `channel` MUST use the canonical Canvas URI form documented by
+/// {@link OpenCanvasParams}.
+///
+/// Clients MUST NOT call this command unless the selected agent advertises
+/// {@link AgentCapabilities.canvases}.
+///
+/// Stability: 1.0 - Early development.
public struct DisposeCanvasParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index a4c221db3..8d413192f 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1531,7 +1531,9 @@ public struct AgentCapabilities: Codable, Sendable {
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
/// registry metadata while the Canvas channel remains experimental. This
/// capability does not gate server-published Canvas references.
- public var canvases: [String: AnyCodable]?
+ ///
+ /// Stability: 1.0 - Early development.
+ public var canvases: CanvasesCapability?
/// The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
/// session starts with. An empty object `{}` advertises multi-chat without
@@ -1550,7 +1552,7 @@ public struct AgentCapabilities: Codable, Sendable {
public var multipleWorkingDirectories: MultipleWorkingDirectoriesCapability?
public init(
- canvases: [String: AnyCodable]? = nil,
+ canvases: CanvasesCapability? = nil,
multipleChats: MultipleChatsCapability? = nil,
multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = nil
) {
@@ -1560,6 +1562,18 @@ public struct AgentCapabilities: Codable, Sendable {
}
}
+/// Presence capability for the experimental Canvas registry and lifecycle
+/// commands.
+///
+/// Stability: 1.0 - Early development.
+public struct CanvasesCapability: Codable, Sendable {
+
+ public init(
+
+ ) {
+ }
+}
+
public struct MultipleChatsCapability: Codable, Sendable {
/// The agent can fork a chat from a specific turn. When absent or `false`,
/// clients MUST NOT pass a {@link ChatSource} with `kind: "fork"` to
@@ -1924,6 +1938,8 @@ public struct ChatState: Codable, Sendable {
/// registry through `chat/canvasRegistryChanged`. An empty
/// {@link CanvasRegistry.extensions} array is a loaded registry with no
/// available canvas types.
+ ///
+ /// Stability: 1.0 - Early development.
public var canvasRegistry: CanvasRegistry?
/// Live canvases currently exposed by this chat.
///
@@ -6324,7 +6340,9 @@ public struct CanvasRegistry: Codable, Sendable {
}
public struct CanvasReference: Codable, Sendable {
- /// Canvas channel URI. Subscribe to this resource for the full state.
+ /// Canonical Canvas channel URI. Subscribe to this resource for the full
+ /// state. It uses the `ahp-canvas:/[/...]` form documented
+ /// by `openCanvas`.
public var resource: String
public init(
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index e58ff713a..3aa29625b 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -1015,9 +1015,8 @@
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
"canvases": {
- "type": "object",
- "additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
+ "$ref": "#/$defs/CanvasesCapability",
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references.\n\nStability: 1.0 - Early development."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -1029,6 +1028,11 @@
}
}
},
+ "CanvasesCapability": {
+ "type": "object",
+ "description": "Presence capability for the experimental Canvas registry and lifecycle\ncommands.\n\nStability: 1.0 - Early development.",
+ "properties": {}
+ },
"MultipleChatsCapability": {
"type": "object",
"description": "Options for the {@link AgentCapabilities.multipleChats} capability.",
@@ -2979,7 +2983,7 @@
},
"canvasRegistry": {
"$ref": "#/$defs/CanvasRegistry",
- "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types.\n\nStability: 1.0 - Early development."
},
"canvases": {
"type": "array",
@@ -5242,7 +5246,7 @@
"properties": {
"resource": {
"$ref": "#/$defs/URI",
- "description": "Canvas channel URI. Subscribe to this resource for the full state."
+ "description": "Canonical Canvas channel URI. Subscribe to this resource for the full\nstate. It uses the `ahp-canvas:/[/...]` form documented\nby `openCanvas`."
}
},
"required": [
diff --git a/schema/state.schema.json b/schema/state.schema.json
index b7ddf9fa8..bbbe346fa 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -624,9 +624,8 @@
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
"canvases": {
- "type": "object",
- "additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
+ "$ref": "#/$defs/CanvasesCapability",
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references.\n\nStability: 1.0 - Early development."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -638,6 +637,11 @@
}
}
},
+ "CanvasesCapability": {
+ "type": "object",
+ "description": "Presence capability for the experimental Canvas registry and lifecycle\ncommands.\n\nStability: 1.0 - Early development.",
+ "properties": {}
+ },
"MultipleChatsCapability": {
"type": "object",
"description": "Options for the {@link AgentCapabilities.multipleChats} capability.",
@@ -2588,7 +2592,7 @@
},
"canvasRegistry": {
"$ref": "#/$defs/CanvasRegistry",
- "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types.\n\nStability: 1.0 - Early development."
},
"canvases": {
"type": "array",
@@ -4851,7 +4855,7 @@
"properties": {
"resource": {
"$ref": "#/$defs/URI",
- "description": "Canvas channel URI. Subscribe to this resource for the full state."
+ "description": "Canonical Canvas channel URI. Subscribe to this resource for the full\nstate. It uses the `ahp-canvas:/[/...]` form documented\nby `openCanvas`."
}
},
"required": [
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index 4fba24edc..b9bde4c86 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -781,6 +781,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin
{ name: 'RootConfigState', mutable: true },
{ name: 'AgentInfo' },
{ name: 'AgentCapabilities' },
+ { name: 'CanvasesCapability' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
{ name: 'SessionModelInfo' },
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index 073ad0238..4fe9aa81b 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -749,6 +749,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin
{ name: 'RootConfigState' },
{ name: 'AgentInfo' },
{ name: 'AgentCapabilities' },
+ { name: 'CanvasesCapability' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
{ name: 'SessionModelInfo' },
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index cba037bd9..4717edf41 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -1008,6 +1008,7 @@ const STATE_STRUCTS = [
'ChannelFlowControl', 'ResumedChannelSubscription',
'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo',
'AgentCapabilities',
+ 'CanvasesCapability',
'MultipleChatsCapability',
'MultipleWorkingDirectoriesCapability',
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts
index 144d7775d..5235108a6 100644
--- a/scripts/generate-markdown.ts
+++ b/scripts/generate-markdown.ts
@@ -1017,6 +1017,7 @@ function generateCanvasChannelPage(project: Project): string {
currentPage = 'canvas';
const stateSf = findChannelSourceFile(project, 'channels-canvas', 'state.ts');
const actionsSf = findChannelSourceFile(project, 'channels-canvas', 'actions.ts');
+ const commandsSf = findChannelSourceFile(project, 'channels-canvas', 'commands.ts');
const lines: string[] = [GENERATED_HEADER];
lines.push('# Canvas Channel\n');
@@ -1034,6 +1035,11 @@ function generateCanvasChannelPage(project: Project): string {
lines.push(schemaLink('actions.schema.json'));
lines.push(emitActionsSection([actionsSf]));
}
+ if (commandsSf) {
+ lines.push('## Commands\n');
+ lines.push(schemaLink('commands.schema.json'));
+ lines.push(emitCommandsSection(project, [commandsSf]));
+ }
return lines.join('\n');
}
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index c4d3dfdb2..a3b50887b 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -879,6 +879,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str
{ name: 'RootConfigState' },
{ name: 'AgentInfo' },
{ name: 'AgentCapabilities' },
+ { name: 'CanvasesCapability' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
{ name: 'SessionModelInfo' },
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index db1224f01..506c9c73e 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -701,6 +701,7 @@ const STATE_STRUCTS = [
'ChannelFlowControl', 'ResumedChannelSubscription',
'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo',
'AgentCapabilities',
+ 'CanvasesCapability',
'MultipleChatsCapability',
'MultipleWorkingDirectoriesCapability',
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
diff --git a/scripts/read-stability.test.ts b/scripts/read-stability.test.ts
index 92985e568..72eb0554a 100644
--- a/scripts/read-stability.test.ts
+++ b/scripts/read-stability.test.ts
@@ -113,7 +113,7 @@ test('native client docs preserve declaration and entrypoint stability', () => {
['clients/dotnet/src/AgentHostProtocol.Abstractions/Generated', 'Commands.generated.cs', 'Notifications.generated.cs'],
];
for (const [directory, commands, notifications] of clients) {
- for (const [file, expected] of [[commands, 14], [notifications, 6]] as const) {
+ for (const [file, expected] of [[commands, 16], [notifications, 6]] as const) {
const content = readFileSync(resolve(root, directory, file), 'utf8');
assert.equal(content.split(marker).length - 1, expected, `${directory}/${file}`);
}
diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts
index 89aaa3dc8..f3211cb69 100644
--- a/types/channels-canvas/commands.ts
+++ b/types/channels-canvas/commands.ts
@@ -21,6 +21,11 @@ import type { ChatState } from '../channels-chat/state.js';
* Repeating this command with the same `channel` focuses or refreshes that
* logical instance rather than creating a duplicate. Reusing the channel for
* a different chat, extension, or canvas type MUST fail with `Conflict`.
+ * Canvas channels use the canonical form
+ * `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
+ * query, or fragment, and one or more non-empty path segments containing only
+ * URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
+ * non-canonical channels so URI parsing cannot change the resource identity.
*
* The null result is not presentation state. Clients MUST observe
* {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
@@ -34,11 +39,12 @@ import type { ChatState } from '../channels-chat/state.js';
* @direction Client → Server
* @messageType Request
* @version 1
+ * @stability 1.0
*/
export interface OpenCanvasParams extends BaseParams {
/**
- * Client-chosen URI for the canvas resource, for example
- * `ahp-canvas:/`.
+ * Client-chosen canonical URI for the canvas resource, for example
+ * `ahp-canvas:/`. See the command-level canonical-form requirement.
*/
channel: URI;
/** Exact chat URI that owns the canvas instance. */
@@ -57,6 +63,8 @@ export interface OpenCanvasParams extends BaseParams {
* The server removes the resource from the owning chat's
* {@link ChatState.canvases} references and notifies the provider that owns the
* instance. Repeating this command after the resource is gone is a no-op.
+ * The inherited `channel` MUST use the canonical Canvas URI form documented by
+ * {@link OpenCanvasParams}.
*
* Clients MUST NOT call this command unless the selected agent advertises
* {@link AgentCapabilities.canvases}.
@@ -66,5 +74,6 @@ export interface OpenCanvasParams extends BaseParams {
* @direction Client → Server
* @messageType Request
* @version 1
+ * @stability 1.0
*/
export interface DisposeCanvasParams extends BaseParams {}
diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts
index 34a7e4c47..b3c089c17 100644
--- a/types/channels-canvas/state.ts
+++ b/types/channels-canvas/state.ts
@@ -101,7 +101,11 @@ export interface CanvasRegistry {
* @category Canvas State
*/
export interface CanvasReference {
- /** Canvas channel URI. Subscribe to this resource for the full state. */
+ /**
+ * Canonical Canvas channel URI. Subscribe to this resource for the full
+ * state. It uses the `ahp-canvas:/[/...]` form documented
+ * by `openCanvas`.
+ */
resource: URI;
}
diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts
index cf6503302..6f7b93cc8 100644
--- a/types/channels-chat/actions.ts
+++ b/types/channels-chat/actions.ts
@@ -64,6 +64,7 @@ interface ToolCallActionBase {
*
* @category Chat Actions
* @version 1
+ * @stability 1.0
* @clientDispatchable
*/
export interface ChatTurnStartedAction {
@@ -99,6 +100,7 @@ export interface ChatTurnStartedAction {
*
* @category Chat Actions
* @version 1
+ * @stability 1.0
*/
export interface ChatDeltaAction {
type: ActionType.ChatDelta;
diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts
index b9a4856b1..8f836a363 100644
--- a/types/channels-chat/state.ts
+++ b/types/channels-chat/state.ts
@@ -138,6 +138,7 @@ export interface ChatState {
* registry through `chat/canvasRegistryChanged`. An empty
* {@link CanvasRegistry.extensions} array is a loaded registry with no
* available canvas types.
+ * @stability 1.0
*/
canvasRegistry?: CanvasRegistry;
/**
diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts
index d4893fc14..e252aae2e 100644
--- a/types/channels-root/state.ts
+++ b/types/channels-root/state.ts
@@ -118,8 +118,9 @@ export interface AgentCapabilities {
* Clients MUST treat absence as unsupported and SHOULD tolerate additive
* registry metadata while the Canvas channel remains experimental. This
* capability does not gate server-published Canvas references.
+ * @stability 1.0
*/
- canvases?: Record;
+ canvases?: CanvasesCapability;
/**
* The agent can host more than one concurrent chat per session. When absent,
* clients MUST NOT call `createChat` to open chats beyond the default one the
@@ -142,6 +143,15 @@ export interface AgentCapabilities {
multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability;
}
+/**
+ * Presence capability for the experimental Canvas registry and lifecycle
+ * commands.
+ *
+ * @category Root State
+ * @stability 1.0
+ */
+export interface CanvasesCapability {}
+
/**
* Options for the {@link AgentCapabilities.multipleChats} capability.
*
From e9fbb3dbc45feb6a546a8395730d6a8bbe39df68 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Sat, 31 Oct 2026 02:30:07 -0700
Subject: [PATCH 09/12] canvas: align protocol with review guidance
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Generated/Commands.generated.cs | 29 ++---
.../JsonSerializerContext.generated.cs | 1 -
.../Generated/State.generated.cs | 100 +++---------------
clients/go/ahptypes/canvas_capability_test.go | 23 ----
clients/go/ahptypes/commands.generated.go | 23 ++--
clients/go/ahptypes/state.generated.go | 53 +++-------
.../generated/Actions.generated.kt | 38 ++++---
.../generated/Commands.generated.kt | 23 ++--
.../generated/Messages.generated.kt | 6 ++
.../generated/State.generated.kt | 62 ++---------
clients/rust/crates/ahp-types/src/commands.rs | 23 ++--
clients/rust/crates/ahp-types/src/state.rs | 87 +++------------
.../Generated/Actions.generated.swift | 30 ++++--
.../Generated/Commands.generated.swift | 23 ++--
.../Generated/Messages.generated.swift | 8 ++
.../Generated/State.generated.swift | 67 ++----------
.../Sources/AgentHostProtocol/Reducers.swift | 2 +
docs/guide/actions.md | 8 ++
docs/specification/subscriptions.md | 3 +-
schema/notifications.schema.json | 40 +++----
schema/state.schema.json | 40 +++----
scripts/generate-kotlin.ts | 6 ++
scripts/generate-markdown.ts | 4 +
scripts/generate-swift.ts | 8 ++
scripts/read-stability.test.ts | 9 +-
types/channels-canvas/commands.ts | 34 +++---
types/channels-canvas/state.ts | 56 +++-------
types/channels-chat/actions.ts | 18 ++--
...27-chat-canvasregistryrequested-noop.json} | 0
...-canvasregistrychanged-sets-registry.json} | 6 +-
...anvasregistrychanged-clears-registry.json} | 0
...try.json => 086-chat-canvas-registry.json} | 10 +-
...hat-canvas-registry-requested-action.json} | 0
...-chat-canvas-registry-changed-action.json} | 0
...pen-canvas-client-addressed-resource.json} | 0
...ose-canvas-client-addressed-resource.json} | 0
.../091-agent-canvas-capability.json | 54 ++++++++++
37 files changed, 304 insertions(+), 590 deletions(-)
delete mode 100644 clients/go/ahptypes/canvas_capability_test.go
rename types/test-cases/reducers/{293-chat-canvasregistryrequested-noop.json => 327-chat-canvasregistryrequested-noop.json} (100%)
rename types/test-cases/reducers/{294-chat-canvasregistrychanged-sets-registry.json => 328-chat-canvasregistrychanged-sets-registry.json} (88%)
rename types/test-cases/reducers/{295-chat-canvasregistrychanged-clears-registry.json => 329-chat-canvasregistrychanged-clears-registry.json} (100%)
rename types/test-cases/round-trips/{076-chat-canvas-registry.json => 086-chat-canvas-registry.json} (88%)
rename types/test-cases/round-trips/{077-chat-canvas-registry-requested-action.json => 087-chat-canvas-registry-requested-action.json} (100%)
rename types/test-cases/round-trips/{078-chat-canvas-registry-changed-action.json => 088-chat-canvas-registry-changed-action.json} (100%)
rename types/test-cases/round-trips/{079-open-canvas-client-addressed-resource.json => 089-open-canvas-client-addressed-resource.json} (100%)
rename types/test-cases/round-trips/{080-dispose-canvas-client-addressed-resource.json => 090-dispose-canvas-client-addressed-resource.json} (100%)
create mode 100644 types/test-cases/round-trips/091-agent-canvas-capability.json
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index e817d7f74..6cf9e29e6 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1147,31 +1147,24 @@ public sealed record MoveChatResult
public required string Session { get; init; }
}
-/// Opens or focuses one client-addressed canvas resource in an exact chat.
+/// Opens one client-addressed canvas resource in a chat.
///
/// The resulting live instance is published through the chat's
/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-/// Repeating this command with the same `channel` focuses or refreshes that
-/// logical instance rather than creating a duplicate. Reusing the channel for
+/// Repeating this command with the same `channel` is idempotent. Reusing it for
/// a different chat, extension, or canvas type MUST fail with `Conflict`.
-/// Canvas channels use the canonical form
-/// `ahp-canvas:/<segment>[/<segment>...]`: the lowercase scheme, no authority,
-/// query, or fragment, and one or more non-empty path segments containing only
-/// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
-/// non-canonical channels so URI parsing cannot change the resource identity.
-///
-/// The null result is not presentation state. Clients MUST observe
+/// Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
/// authoritative membership, availability, and presentation state.
///
-/// Clients MUST NOT call this command unless the selected agent advertises
+/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.
public sealed record OpenCanvasParams
{
- /// Client-chosen canonical URI for the canvas resource, for example
- /// `ahp-canvas:/<uuid>`. See the command-level canonical-form requirement.
+ /// Client-chosen URI for the canvas resource, for example
+ /// `ahp-canvas:/<uuid>`.
public required string Channel { get; init; }
/// Optional JSON-serializable metadata associated with this request.
@@ -1193,18 +1186,16 @@ public sealed record OpenCanvasParams
/// Closes and disposes one live canvas resource.
///
/// The server removes the resource from the owning chat's
-/// {@link ChatState.canvases} references and notifies the provider that owns the
-/// instance. Repeating this command after the resource is gone is a no-op.
-/// The inherited `channel` MUST use the canonical Canvas URI form documented by
-/// {@link OpenCanvasParams}.
+/// {@link ChatState.canvases} references. Repeating this command after the
+/// resource is gone is a no-op.
///
-/// Clients MUST NOT call this command unless the selected agent advertises
+/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.
public sealed record DisposeCanvasParams
{
- /// Channel URI this command targets.
+ /// URI of the canvas resource to dispose.
public required string Channel { get; init; }
/// Optional JSON-serializable metadata associated with this request.
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index c850001c3..afb82dd22 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -85,7 +85,6 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(BackgroundWorkKind))]
[JsonSerializable(typeof(CanvasesCapability))]
[JsonSerializable(typeof(CanvasExtensionDeclaration))]
-[JsonSerializable(typeof(CanvasExtensionSource))]
[JsonSerializable(typeof(CanvasReference))]
[JsonSerializable(typeof(CanvasRegistry))]
[JsonSerializable(typeof(CanvasState))]
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 50b177480..58a5aa7bb 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -176,68 +176,6 @@ public override void Write(Utf8JsonWriter writer, SessionOriginKind value, JsonS
=> writer.WriteStringValue(value.Value);
}
-/// Where the runtime discovered an extension that declares a canvas type.
-[JsonConverter(typeof(CanvasExtensionSourceConverter))]
-public readonly struct CanvasExtensionSource : IEquatable
-{
- private readonly string? _value;
-
- /// Wraps a raw wire value — including one this build does not recognize.
- /// The raw wire string.
- public CanvasExtensionSource(string value)
- {
- _value = value;
- }
-
- /// The raw wire value.
- public string Value => _value ?? string.Empty;
-
- /// Extension installed for the current user.
- public static readonly CanvasExtensionSource User = new CanvasExtensionSource("user");
-
- /// Extension discovered in the current project.
- public static readonly CanvasExtensionSource Project = new CanvasExtensionSource("project");
-
- /// Extension installed only for the current session.
- public static readonly CanvasExtensionSource Session = new CanvasExtensionSource("session");
-
- /// Extension contributed by an installed plugin.
- public static readonly CanvasExtensionSource Plugin = new CanvasExtensionSource("plugin");
-
- /// Extension source is unavailable or not recognized by this protocol version.
- public static readonly CanvasExtensionSource Unspecified = new CanvasExtensionSource("unknown");
-
- ///
- public bool Equals(CanvasExtensionSource other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
-
- ///
- public override bool Equals(object? obj) => obj is CanvasExtensionSource other && Equals(other);
-
- ///
- public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
-
- ///
- public override string ToString() => Value;
-
- /// Ordinal equality over the raw wire value.
- public static bool operator ==(CanvasExtensionSource left, CanvasExtensionSource right) => left.Equals(right);
-
- /// Ordinal inequality over the raw wire value.
- public static bool operator !=(CanvasExtensionSource left, CanvasExtensionSource right) => !left.Equals(right);
-}
-
-/// Reads and writes as its raw wire string, preserving unrecognized values.
-internal sealed class CanvasExtensionSourceConverter : JsonConverter
-{
- ///
- public override CanvasExtensionSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => new CanvasExtensionSource(reader.GetString() ?? throw new JsonException("CanvasExtensionSource expects a JSON string."));
-
- ///
- public override void Write(Utf8JsonWriter writer, CanvasExtensionSource value, JsonSerializerOptions options)
- => writer.WriteStringValue(value.Value);
-}
-
/// Discriminant for {@link ChatOrigin} — how a chat came into existence.
[JsonConverter(typeof(ChatOriginKindConverter))]
public readonly struct ChatOriginKind : IEquatable
@@ -2705,20 +2643,16 @@ public sealed class ChatState
public Dictionary? Meta { get; set; }
}
-/// One extension/provider that declares canvas types for an exact chat.
+/// One namespace that declares canvas types for a chat.
///
-/// The extension metadata is normalized here instead of repeated on every
-/// {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
-/// infer its source or management behavior by parsing it.
+/// `extensionId` is opaque. Clients MUST NOT infer management behavior by
+/// parsing it.
public sealed record CanvasExtensionDeclaration
{
- /// Stable opaque identifier of the extension/provider.
+ /// Stable opaque declaration namespace.
public required string ExtensionId { get; init; }
- /// Where the runtime discovered the extension/provider.
- public CanvasExtensionSource ExtensionSource { get; init; }
-
- /// Human-readable extension/provider name, when available.
+ /// Human-readable namespace title, when available.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ExtensionName { get; init; }
@@ -2729,10 +2663,6 @@ public sealed record CanvasExtensionDeclaration
/// One canvas type currently available to open.
///
-/// This is discovery metadata, not a live canvas instance. Live instances are
-/// represented by {@link CanvasReference} entries on chat state and their
-/// corresponding {@link CanvasState} channels.
-///
/// This experimental object may gain optional discovery metadata. Clients
/// SHOULD ignore fields they do not understand.
public sealed record CanvasTypeDeclaration
@@ -2740,8 +2670,8 @@ public sealed record CanvasTypeDeclaration
/// Provider-local canvas type identifier.
public required string CanvasId { get; init; }
- /// Human-readable canvas type name.
- public required string DisplayName { get; init; }
+ /// Human-readable canvas type title.
+ public required string Title { get; init; }
/// Description of what the canvas type does.
public required string Description { get; init; }
@@ -2750,14 +2680,10 @@ public sealed record CanvasTypeDeclaration
public bool RequiresInput { get; init; }
}
-/// Canvas types currently registered for one exact chat.
-///
-/// This is a compact presentation registry, not extension installation state
-/// and not the list of live instances. The host replaces the complete registry
-/// through `chat/canvasRegistryChanged` whenever provider registration changes.
+/// Canvas types currently registered for one chat.
public sealed record CanvasRegistry
{
- /// Declaring extensions/providers. `extensionId` MUST be unique within this
+ /// Declaring namespaces. `extensionId` MUST be unique within this
/// array. An empty array is an authoritative loaded registry with no types.
public required List Extensions { get; init; }
}
@@ -2769,9 +2695,7 @@ public sealed record CanvasRegistry
/// channel shape.
public sealed record CanvasReference
{
- /// Canonical Canvas channel URI. Subscribe to this resource for the full
- /// state. It uses the `ahp-canvas:/<segment>[/<segment>...]` form documented
- /// by `openCanvas`.
+ /// Canvas channel URI. Subscribe to this resource for the full state.
public required string Resource { get; init; }
}
@@ -2789,10 +2713,10 @@ public sealed class CanvasState
/// identity.
public required string InstanceId { get; set; }
- /// Owning extension/provider identifier.
+ /// Owning declaration namespace.
public required string ExtensionId { get; set; }
- /// Owning extension display name, when available.
+ /// Owning namespace title, when available.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ExtensionName { get; set; }
diff --git a/clients/go/ahptypes/canvas_capability_test.go b/clients/go/ahptypes/canvas_capability_test.go
deleted file mode 100644
index 94b55bd3f..000000000
--- a/clients/go/ahptypes/canvas_capability_test.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package ahptypes
-
-import (
- "encoding/json"
- "testing"
-)
-
-func TestCanvasCapabilityPresenceRoundTrip(t *testing.T) {
- var capabilities AgentCapabilities
- if err := json.Unmarshal([]byte(`{"canvases":{}}`), &capabilities); err != nil {
- t.Fatal(err)
- }
- encoded, err := json.Marshal(capabilities)
- if err != nil {
- t.Fatal(err)
- }
- if string(encoded) != `{"canvases":{}}` {
- t.Fatalf("canvas capability presence was not preserved: %s", encoded)
- }
-}
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 83403062b..49b42752e 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -764,24 +764,17 @@ type MoveChatResult struct {
Session URI `json:"session"`
}
-// Opens or focuses one client-addressed canvas resource in an exact chat.
+// Opens one client-addressed canvas resource in a chat.
//
// The resulting live instance is published through the chat's
// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-// Repeating this command with the same `channel` focuses or refreshes that
-// logical instance rather than creating a duplicate. Reusing the channel for
+// Repeating this command with the same `channel` is idempotent. Reusing it for
// a different chat, extension, or canvas type MUST fail with `Conflict`.
-// Canvas channels use the canonical form
-// `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
-// query, or fragment, and one or more non-empty path segments containing only
-// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
-// non-canonical channels so URI parsing cannot change the resource identity.
-//
-// The null result is not presentation state. Clients MUST observe
+// Clients MUST observe
// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
// authoritative membership, availability, and presentation state.
//
-// Clients MUST NOT call this command unless the selected agent advertises
+// Clients MUST NOT call this command unless the chat's agent advertises
// {@link AgentCapabilities.canvases}.
//
// Stability: 1.0 - Early development.
@@ -802,12 +795,10 @@ type OpenCanvasParams struct {
// Closes and disposes one live canvas resource.
//
// The server removes the resource from the owning chat's
-// {@link ChatState.canvases} references and notifies the provider that owns the
-// instance. Repeating this command after the resource is gone is a no-op.
-// The inherited `channel` MUST use the canonical Canvas URI form documented by
-// {@link OpenCanvasParams}.
+// {@link ChatState.canvases} references. Repeating this command after the
+// resource is gone is a no-op.
//
-// Clients MUST NOT call this command unless the selected agent advertises
+// Clients MUST NOT call this command unless the chat's agent advertises
// {@link AgentCapabilities.canvases}.
//
// Stability: 1.0 - Early development.
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index ec4b5518d..42d60ed79 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -503,22 +503,6 @@ const (
SessionOriginKindAutomation SessionOriginKind = "automation"
)
-// Where the runtime discovered an extension that declares a canvas type.
-type CanvasExtensionSource string
-
-const (
- // Extension installed for the current user.
- CanvasExtensionSourceUser CanvasExtensionSource = "user"
- // Extension discovered in the current project.
- CanvasExtensionSourceProject CanvasExtensionSource = "project"
- // Extension installed only for the current session.
- CanvasExtensionSourceSession CanvasExtensionSource = "session"
- // Extension contributed by an installed plugin.
- CanvasExtensionSourcePlugin CanvasExtensionSource = "plugin"
- // Extension source is unavailable or not recognized by this protocol version.
- CanvasExtensionSourceUnspecified CanvasExtensionSource = "unknown"
-)
-
// Operations the host currently permits for an automation.
//
// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -1528,17 +1512,14 @@ type ChatState struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
-// One extension/provider that declares canvas types for an exact chat.
+// One namespace that declares canvas types for a chat.
//
-// The extension metadata is normalized here instead of repeated on every
-// {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
-// infer its source or management behavior by parsing it.
+// `extensionId` is opaque. Clients MUST NOT infer management behavior by
+// parsing it.
type CanvasExtensionDeclaration struct {
- // Stable opaque identifier of the extension/provider.
+ // Stable opaque declaration namespace.
ExtensionId string `json:"extensionId"`
- // Where the runtime discovered the extension/provider.
- ExtensionSource CanvasExtensionSource `json:"extensionSource"`
- // Human-readable extension/provider name, when available.
+ // Human-readable namespace title, when available.
ExtensionName *string `json:"extensionName,omitempty"`
// Canvas types declared by this extension/provider. `canvasId` MUST be
// unique within this array.
@@ -1547,30 +1528,22 @@ type CanvasExtensionDeclaration struct {
// One canvas type currently available to open.
//
-// This is discovery metadata, not a live canvas instance. Live instances are
-// represented by {@link CanvasReference} entries on chat state and their
-// corresponding {@link CanvasState} channels.
-//
// This experimental object may gain optional discovery metadata. Clients
// SHOULD ignore fields they do not understand.
type CanvasTypeDeclaration struct {
// Provider-local canvas type identifier.
CanvasId string `json:"canvasId"`
- // Human-readable canvas type name.
- DisplayName string `json:"displayName"`
+ // Human-readable canvas type title.
+ Title string `json:"title"`
// Description of what the canvas type does.
Description string `json:"description"`
// Whether opening this canvas requires an input flow.
RequiresInput bool `json:"requiresInput"`
}
-// Canvas types currently registered for one exact chat.
-//
-// This is a compact presentation registry, not extension installation state
-// and not the list of live instances. The host replaces the complete registry
-// through `chat/canvasRegistryChanged` whenever provider registration changes.
+// Canvas types currently registered for one chat.
type CanvasRegistry struct {
- // Declaring extensions/providers. `extensionId` MUST be unique within this
+ // Declaring namespaces. `extensionId` MUST be unique within this
// array. An empty array is an authoritative loaded registry with no types.
Extensions []CanvasExtensionDeclaration `json:"extensions"`
}
@@ -1581,9 +1554,7 @@ type CanvasRegistry struct {
// canvas presentation model can evolve without changing the stable chat
// channel shape.
type CanvasReference struct {
- // Canonical Canvas channel URI. Subscribe to this resource for the full
- // state. It uses the `ahp-canvas:/[/...]` form documented
- // by `openCanvas`.
+ // Canvas channel URI. Subscribe to this resource for the full state.
Resource URI `json:"resource"`
}
@@ -1599,9 +1570,9 @@ type CanvasState struct {
// URI. They MUST NOT treat this provider identifier as a second resource
// identity.
InstanceId string `json:"instanceId"`
- // Owning extension/provider identifier.
+ // Owning declaration namespace.
ExtensionId string `json:"extensionId"`
- // Owning extension display name, when available.
+ // Owning namespace title, when available.
ExtensionName *string `json:"extensionName,omitempty"`
// Provider-local canvas type identifier.
CanvasId string `json:"canvasId"`
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
index 9818b20b0..712a65b2d 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
@@ -258,13 +258,6 @@ data class SessionDefaultChatChangedAction(
val defaultChat: String? = null
)
-/**
- * A new message has been sent to the agent, and a new turn starts.
- *
- * A client is only allowed to send {@link MessageKind.User} messages.
- *
- * Stability: 1.0 - Early development.
- */
@Serializable
data class ChatTurnStartedAction(
val type: ActionType,
@@ -298,14 +291,6 @@ data class ChatTurnStartedAction(
val meta: Map? = null
)
-/**
- * Streaming text chunk from the assistant, appended to a specific response part.
- *
- * The server MUST first emit a `chat/responsePart` to create the target
- * markdown part, then use this action to append text to it.
- *
- * Stability: 1.0 - Early development.
- */
@Serializable
data class ChatDeltaAction(
val type: ActionType,
@@ -810,11 +795,34 @@ data class ChatCanvasesChangedAction(
val canvases: List? = null
)
+/**
+ * Requests materialization of the chat's Canvas type registry.
+ *
+ * A host MUST reject the action when the chat's agent does not advertise
+ * {@link AgentCapabilities.canvases} or when the chat cannot materialize the
+ * registry. Materialization MUST NOT send a user message or invoke a model.
+ *
+ * The reducer is a no-op. The host remains authoritative and publishes the
+ * complete result through
+ * {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
+ * Clients do not need to dispatch this action when
+ * {@link ChatState.canvasRegistry} is already present.
+ *
+ * Stability: 1.0 - Early development.
+ */
@Serializable
data class ChatCanvasRegistryRequestedAction(
val type: ActionType
)
+/**
+ * The Canvas type registry for this chat changed.
+ *
+ * Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
+ * Set to `undefined` when the registry is no longer materialized or available.
+ *
+ * Stability: 1.0 - Early development.
+ */
@Serializable
data class ChatCanvasRegistryChangedAction(
val type: ActionType,
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index bd6233845..a8012f50b 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1025,24 +1025,17 @@ data class MoveChatResult(
)
/**
- * Opens or focuses one client-addressed canvas resource in an exact chat.
+ * Opens one client-addressed canvas resource in a chat.
*
* The resulting live instance is published through the chat's
* {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
- * Repeating this command with the same `channel` focuses or refreshes that
- * logical instance rather than creating a duplicate. Reusing the channel for
+ * Repeating this command with the same `channel` is idempotent. Reusing it for
* a different chat, extension, or canvas type MUST fail with `Conflict`.
- * Canvas channels use the canonical form
- * `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
- * query, or fragment, and one or more non-empty path segments containing only
- * URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
- * non-canonical channels so URI parsing cannot change the resource identity.
- *
- * The null result is not presentation state. Clients MUST observe
+ * Clients MUST observe
* {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
* authoritative membership, availability, and presentation state.
*
- * Clients MUST NOT call this command unless the selected agent advertises
+ * Clients MUST NOT call this command unless the chat's agent advertises
* {@link AgentCapabilities.canvases}.
*
* Stability: 1.0 - Early development.
@@ -1077,12 +1070,10 @@ data class OpenCanvasParams(
* Closes and disposes one live canvas resource.
*
* The server removes the resource from the owning chat's
- * {@link ChatState.canvases} references and notifies the provider that owns the
- * instance. Repeating this command after the resource is gone is a no-op.
- * The inherited `channel` MUST use the canonical Canvas URI form documented by
- * {@link OpenCanvasParams}.
+ * {@link ChatState.canvases} references. Repeating this command after the
+ * resource is gone is a no-op.
*
- * Clients MUST NOT call this command unless the selected agent advertises
+ * Clients MUST NOT call this command unless the chat's agent advertises
* {@link AgentCapabilities.canvases}.
*
* Stability: 1.0 - Early development.
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt
index 3209e3e15..8cf4f3b46 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt
@@ -140,6 +140,12 @@ object AhpCommands {
fun disposeTerminal(id: Long, params: DisposeTerminalParams): JsonRpcRequest =
JsonRpcRequest(id = id, method = "disposeTerminal", params = params)
+ fun openCanvas(id: Long, params: OpenCanvasParams): JsonRpcRequest =
+ JsonRpcRequest(id = id, method = "openCanvas", params = params)
+
+ fun disposeCanvas(id: Long, params: DisposeCanvasParams): JsonRpcRequest =
+ JsonRpcRequest(id = id, method = "disposeCanvas", params = params)
+
fun resolveSessionConfig(id: Long, params: ResolveSessionConfigParams): JsonRpcRequest =
JsonRpcRequest(id = id, method = "resolveSessionConfig", params = params)
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 0e854032b..c52477171 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1066,46 +1066,6 @@ internal object SessionOriginKindSerializer : KSerializer {
SessionOriginKind(decoder.decodeString())
}
-/**
- * Where the runtime discovered an extension that declares a canvas type.
- */
-@Serializable(with = CanvasExtensionSourceSerializer::class)
-@JvmInline
-value class CanvasExtensionSource(val rawValue: String) {
- companion object {
- /**
- * Extension installed for the current user.
- */
- val USER: CanvasExtensionSource = CanvasExtensionSource("user")
- /**
- * Extension discovered in the current project.
- */
- val PROJECT: CanvasExtensionSource = CanvasExtensionSource("project")
- /**
- * Extension installed only for the current session.
- */
- val SESSION: CanvasExtensionSource = CanvasExtensionSource("session")
- /**
- * Extension contributed by an installed plugin.
- */
- val PLUGIN: CanvasExtensionSource = CanvasExtensionSource("plugin")
- /**
- * Extension source is unavailable or not recognized by this protocol version.
- */
- val UNSPECIFIED: CanvasExtensionSource = CanvasExtensionSource("unknown")
- }
-}
-
-internal object CanvasExtensionSourceSerializer : KSerializer {
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("CanvasExtensionSource", PrimitiveKind.STRING)
- override fun serialize(encoder: Encoder, value: CanvasExtensionSource) {
- encoder.encodeString(value.rawValue)
- }
- override fun deserialize(decoder: Decoder): CanvasExtensionSource =
- CanvasExtensionSource(decoder.decodeString())
-}
-
/**
* Operations the host currently permits for an automation.
*
@@ -2322,15 +2282,11 @@ data class BackgroundSubagentWork(
@Serializable
data class CanvasExtensionDeclaration(
/**
- * Stable opaque identifier of the extension/provider.
+ * Stable opaque declaration namespace.
*/
val extensionId: String,
/**
- * Where the runtime discovered the extension/provider.
- */
- val extensionSource: CanvasExtensionSource,
- /**
- * Human-readable extension/provider name, when available.
+ * Human-readable namespace title, when available.
*/
val extensionName: String? = null,
/**
@@ -2347,9 +2303,9 @@ data class CanvasTypeDeclaration(
*/
val canvasId: String,
/**
- * Human-readable canvas type name.
+ * Human-readable canvas type title.
*/
- val displayName: String,
+ val title: String,
/**
* Description of what the canvas type does.
*/
@@ -2363,7 +2319,7 @@ data class CanvasTypeDeclaration(
@Serializable
data class CanvasRegistry(
/**
- * Declaring extensions/providers. `extensionId` MUST be unique within this
+ * Declaring namespaces. `extensionId` MUST be unique within this
* array. An empty array is an authoritative loaded registry with no types.
*/
val extensions: List
@@ -2372,9 +2328,7 @@ data class CanvasRegistry(
@Serializable
data class CanvasReference(
/**
- * Canonical Canvas channel URI. Subscribe to this resource for the full
- * state. It uses the `ahp-canvas:/[/...]` form documented
- * by `openCanvas`.
+ * Canvas channel URI. Subscribe to this resource for the full state.
*/
val resource: String
)
@@ -2390,11 +2344,11 @@ data class CanvasState(
*/
val instanceId: String,
/**
- * Owning extension/provider identifier.
+ * Owning declaration namespace.
*/
val extensionId: String,
/**
- * Owning extension display name, when available.
+ * Owning namespace title, when available.
*/
val extensionName: String? = null,
/**
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index d946fc3f6..b701e926c 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -1055,24 +1055,17 @@ pub struct MoveChatResult {
pub session: Uri,
}
-/// Opens or focuses one client-addressed canvas resource in an exact chat.
+/// Opens one client-addressed canvas resource in a chat.
///
/// The resulting live instance is published through the chat's
/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-/// Repeating this command with the same `channel` focuses or refreshes that
-/// logical instance rather than creating a duplicate. Reusing the channel for
+/// Repeating this command with the same `channel` is idempotent. Reusing it for
/// a different chat, extension, or canvas type MUST fail with `Conflict`.
-/// Canvas channels use the canonical form
-/// `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
-/// query, or fragment, and one or more non-empty path segments containing only
-/// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
-/// non-canonical channels so URI parsing cannot change the resource identity.
-///
-/// The null result is not presentation state. Clients MUST observe
+/// Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
/// authoritative membership, availability, and presentation state.
///
-/// Clients MUST NOT call this command unless the selected agent advertises
+/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.
@@ -1096,12 +1089,10 @@ pub struct OpenCanvasParams {
/// Closes and disposes one live canvas resource.
///
/// The server removes the resource from the owning chat's
-/// {@link ChatState.canvases} references and notifies the provider that owns the
-/// instance. Repeating this command after the resource is gone is a no-op.
-/// The inherited `channel` MUST use the canonical Canvas URI form documented by
-/// {@link OpenCanvasParams}.
+/// {@link ChatState.canvases} references. Repeating this command after the
+/// resource is gone is a no-op.
///
-/// Clients MUST NOT call this command unless the selected agent advertises
+/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index 6f33aaa99..7e6a0bca2 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1354,56 +1354,6 @@ impl<'de> serde::Deserialize<'de> for SessionOriginKind {
}
}
-/// Where the runtime discovered an extension that declares a canvas type.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum CanvasExtensionSource {
- /// Extension installed for the current user.
- User,
- /// Extension discovered in the current project.
- Project,
- /// Extension installed only for the current session.
- Session,
- /// Extension contributed by an installed plugin.
- Plugin,
- /// Extension source is unavailable or not recognized by this protocol version.
- Unspecified,
- /// Unknown raw value from a newer protocol version, preserved verbatim.
- Unknown(String),
-}
-
-impl serde::Serialize for CanvasExtensionSource {
- fn serialize(&self, serializer: S) -> Result
- where
- S: serde::Serializer,
- {
- match self {
- Self::User => serializer.serialize_str("user"),
- Self::Project => serializer.serialize_str("project"),
- Self::Session => serializer.serialize_str("session"),
- Self::Plugin => serializer.serialize_str("plugin"),
- Self::Unspecified => serializer.serialize_str("unknown"),
- Self::Unknown(value) => serializer.serialize_str(value),
- }
- }
-}
-
-impl<'de> serde::Deserialize<'de> for CanvasExtensionSource {
- fn deserialize(deserializer: D) -> Result
- where
- D: serde::Deserializer<'de>,
- {
- let raw = ::deserialize(deserializer)?;
- Ok(match raw.as_str() {
- "user" => Self::User,
- "project" => Self::Project,
- "session" => Self::Session,
- "plugin" => Self::Plugin,
- "unknown" => Self::Unspecified,
- _ => Self::Unknown(raw),
- })
- }
-}
-
/// Operations the host currently permits for an automation.
///
/// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -2243,19 +2193,16 @@ pub struct ChatState {
pub meta: Option,
}
-/// One extension/provider that declares canvas types for an exact chat.
+/// One namespace that declares canvas types for a chat.
///
-/// The extension metadata is normalized here instead of repeated on every
-/// {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
-/// infer its source or management behavior by parsing it.
+/// `extensionId` is opaque. Clients MUST NOT infer management behavior by
+/// parsing it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasExtensionDeclaration {
- /// Stable opaque identifier of the extension/provider.
+ /// Stable opaque declaration namespace.
pub extension_id: String,
- /// Where the runtime discovered the extension/provider.
- pub extension_source: CanvasExtensionSource,
- /// Human-readable extension/provider name, when available.
+ /// Human-readable namespace title, when available.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extension_name: Option,
/// Canvas types declared by this extension/provider. `canvasId` MUST be
@@ -2265,10 +2212,6 @@ pub struct CanvasExtensionDeclaration {
/// One canvas type currently available to open.
///
-/// This is discovery metadata, not a live canvas instance. Live instances are
-/// represented by {@link CanvasReference} entries on chat state and their
-/// corresponding {@link CanvasState} channels.
-///
/// This experimental object may gain optional discovery metadata. Clients
/// SHOULD ignore fields they do not understand.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -2276,23 +2219,19 @@ pub struct CanvasExtensionDeclaration {
pub struct CanvasTypeDeclaration {
/// Provider-local canvas type identifier.
pub canvas_id: String,
- /// Human-readable canvas type name.
- pub display_name: String,
+ /// Human-readable canvas type title.
+ pub title: String,
/// Description of what the canvas type does.
pub description: String,
/// Whether opening this canvas requires an input flow.
pub requires_input: bool,
}
-/// Canvas types currently registered for one exact chat.
-///
-/// This is a compact presentation registry, not extension installation state
-/// and not the list of live instances. The host replaces the complete registry
-/// through `chat/canvasRegistryChanged` whenever provider registration changes.
+/// Canvas types currently registered for one chat.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasRegistry {
- /// Declaring extensions/providers. `extensionId` MUST be unique within this
+ /// Declaring namespaces. `extensionId` MUST be unique within this
/// array. An empty array is an authoritative loaded registry with no types.
pub extensions: Vec,
}
@@ -2305,9 +2244,7 @@ pub struct CanvasRegistry {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasReference {
- /// Canonical Canvas channel URI. Subscribe to this resource for the full
- /// state. It uses the `ahp-canvas:/[/...]` form documented
- /// by `openCanvas`.
+ /// Canvas channel URI. Subscribe to this resource for the full state.
pub resource: Uri,
}
@@ -2325,9 +2262,9 @@ pub struct CanvasState {
/// URI. They MUST NOT treat this provider identifier as a second resource
/// identity.
pub instance_id: String,
- /// Owning extension/provider identifier.
+ /// Owning declaration namespace.
pub extension_id: String,
- /// Owning extension display name, when available.
+ /// Owning namespace title, when available.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extension_name: Option,
/// Provider-local canvas type identifier.
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
index 724479b71..c8bb222d0 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
@@ -524,11 +524,6 @@ public struct SessionDefaultChatChangedAction: Codable, Sendable {
}
}
-/// A new message has been sent to the agent, and a new turn starts.
-///
-/// A client is only allowed to send {@link MessageKind.User} messages.
-///
-/// Stability: 1.0 - Early development.
public struct ChatTurnStartedAction: Codable, Sendable {
public var type: ActionType
/// Turn identifier
@@ -575,12 +570,6 @@ public struct ChatTurnStartedAction: Codable, Sendable {
}
}
-/// Streaming text chunk from the assistant, appended to a specific response part.
-///
-/// The server MUST first emit a `chat/responsePart` to create the target
-/// markdown part, then use this action to append text to it.
-///
-/// Stability: 1.0 - Early development.
public struct ChatDeltaAction: Codable, Sendable {
public var type: ActionType
/// Turn identifier
@@ -1314,6 +1303,19 @@ public struct ChatCanvasesChangedAction: Codable, Sendable {
}
}
+/// Requests materialization of the chat's Canvas type registry.
+///
+/// A host MUST reject the action when the chat's agent does not advertise
+/// {@link AgentCapabilities.canvases} or when the chat cannot materialize the
+/// registry. Materialization MUST NOT send a user message or invoke a model.
+///
+/// The reducer is a no-op. The host remains authoritative and publishes the
+/// complete result through
+/// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
+/// Clients do not need to dispatch this action when
+/// {@link ChatState.canvasRegistry} is already present.
+///
+/// Stability: 1.0 - Early development.
public struct ChatCanvasRegistryRequestedAction: Codable, Sendable {
public var type: ActionType
@@ -1324,6 +1326,12 @@ public struct ChatCanvasRegistryRequestedAction: Codable, Sendable {
}
}
+/// The Canvas type registry for this chat changed.
+///
+/// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
+/// Set to `undefined` when the registry is no longer materialized or available.
+///
+/// Stability: 1.0 - Early development.
public struct ChatCanvasRegistryChangedAction: Codable, Sendable {
public var type: ActionType
/// Authoritative registry, or `undefined` to clear it.
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index 90628d3ac..b8adc0d7a 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1154,24 +1154,17 @@ public struct MoveChatResult: Codable, Sendable {
}
}
-/// Opens or focuses one client-addressed canvas resource in an exact chat.
+/// Opens one client-addressed canvas resource in a chat.
///
/// The resulting live instance is published through the chat's
/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-/// Repeating this command with the same `channel` focuses or refreshes that
-/// logical instance rather than creating a duplicate. Reusing the channel for
+/// Repeating this command with the same `channel` is idempotent. Reusing it for
/// a different chat, extension, or canvas type MUST fail with `Conflict`.
-/// Canvas channels use the canonical form
-/// `ahp-canvas:/[/...]`: the lowercase scheme, no authority,
-/// query, or fragment, and one or more non-empty path segments containing only
-/// URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
-/// non-canonical channels so URI parsing cannot change the resource identity.
-///
-/// The null result is not presentation state. Clients MUST observe
+/// Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
/// authoritative membership, availability, and presentation state.
///
-/// Clients MUST NOT call this command unless the selected agent advertises
+/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.
@@ -1214,12 +1207,10 @@ public struct OpenCanvasParams: Codable, Sendable {
/// Closes and disposes one live canvas resource.
///
/// The server removes the resource from the owning chat's
-/// {@link ChatState.canvases} references and notifies the provider that owns the
-/// instance. Repeating this command after the resource is gone is a no-op.
-/// The inherited `channel` MUST use the canonical Canvas URI form documented by
-/// {@link OpenCanvasParams}.
+/// {@link ChatState.canvases} references. Repeating this command after the
+/// resource is gone is a no-op.
///
-/// Clients MUST NOT call this command unless the selected agent advertises
+/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift
index 956693651..9f43ea302 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift
@@ -149,6 +149,14 @@ public enum AHPCommands {
public static func disposeTerminal(id: Int, params: DisposeTerminalParams) -> JsonRpcRequest {
JsonRpcRequest(id: id, method: "disposeTerminal", params: params)
}
+
+ public static func openCanvas(id: Int, params: OpenCanvasParams) -> JsonRpcRequest {
+ JsonRpcRequest(id: id, method: "openCanvas", params: params)
+ }
+
+ public static func disposeCanvas(id: Int, params: DisposeCanvasParams) -> JsonRpcRequest {
+ JsonRpcRequest(id: id, method: "disposeCanvas", params: params)
+ }
}
/// Typed helper for constructing client → server notifications.
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index 8d413192f..9eb5add39 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1049,47 +1049,6 @@ public enum SessionOriginKind: Codable, Sendable, Equatable {
}
}
-/// Where the runtime discovered an extension that declares a canvas type.
-public enum CanvasExtensionSource: Codable, Sendable, Equatable {
- /// Extension installed for the current user.
- case user
- /// Extension discovered in the current project.
- case project
- /// Extension installed only for the current session.
- case session
- /// Extension contributed by an installed plugin.
- case plugin
- /// Extension source is unavailable or not recognized by this protocol version.
- case unspecified
- /// Unknown raw value from a newer protocol version, preserved verbatim.
- case unknown(String)
-
- public init(from decoder: Decoder) throws {
- let container = try decoder.singleValueContainer()
- let raw = try container.decode(String.self)
- switch raw {
- case "user": self = .user
- case "project": self = .project
- case "session": self = .session
- case "plugin": self = .plugin
- case "unknown": self = .unspecified
- default: self = .unknown(raw)
- }
- }
-
- public func encode(to encoder: Encoder) throws {
- var container = encoder.singleValueContainer()
- switch self {
- case .user: try container.encode("user")
- case .project: try container.encode("project")
- case .session: try container.encode("session")
- case .plugin: try container.encode("plugin")
- case .unspecified: try container.encode("unknown")
- case .unknown(let raw): try container.encode(raw)
- }
- }
-}
-
/// Operations the host currently permits for an automation.
///
/// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -6281,11 +6240,9 @@ public struct EmptyState: Codable, Sendable {
}
public struct CanvasExtensionDeclaration: Codable, Sendable {
- /// Stable opaque identifier of the extension/provider.
+ /// Stable opaque declaration namespace.
public var extensionId: String
- /// Where the runtime discovered the extension/provider.
- public var extensionSource: CanvasExtensionSource
- /// Human-readable extension/provider name, when available.
+ /// Human-readable namespace title, when available.
public var extensionName: String?
/// Canvas types declared by this extension/provider. `canvasId` MUST be
/// unique within this array.
@@ -6293,12 +6250,10 @@ public struct CanvasExtensionDeclaration: Codable, Sendable {
public init(
extensionId: String,
- extensionSource: CanvasExtensionSource,
extensionName: String? = nil,
canvases: [CanvasTypeDeclaration]
) {
self.extensionId = extensionId
- self.extensionSource = extensionSource
self.extensionName = extensionName
self.canvases = canvases
}
@@ -6307,8 +6262,8 @@ public struct CanvasExtensionDeclaration: Codable, Sendable {
public struct CanvasTypeDeclaration: Codable, Sendable {
/// Provider-local canvas type identifier.
public var canvasId: String
- /// Human-readable canvas type name.
- public var displayName: String
+ /// Human-readable canvas type title.
+ public var title: String
/// Description of what the canvas type does.
public var description: String
/// Whether opening this canvas requires an input flow.
@@ -6316,19 +6271,19 @@ public struct CanvasTypeDeclaration: Codable, Sendable {
public init(
canvasId: String,
- displayName: String,
+ title: String,
description: String,
requiresInput: Bool
) {
self.canvasId = canvasId
- self.displayName = displayName
+ self.title = title
self.description = description
self.requiresInput = requiresInput
}
}
public struct CanvasRegistry: Codable, Sendable {
- /// Declaring extensions/providers. `extensionId` MUST be unique within this
+ /// Declaring namespaces. `extensionId` MUST be unique within this
/// array. An empty array is an authoritative loaded registry with no types.
public var extensions: [CanvasExtensionDeclaration]
@@ -6340,9 +6295,7 @@ public struct CanvasRegistry: Codable, Sendable {
}
public struct CanvasReference: Codable, Sendable {
- /// Canonical Canvas channel URI. Subscribe to this resource for the full
- /// state. It uses the `ahp-canvas:/[/...]` form documented
- /// by `openCanvas`.
+ /// Canvas channel URI. Subscribe to this resource for the full state.
public var resource: String
public init(
@@ -6359,9 +6312,9 @@ public struct CanvasState: Codable, Sendable {
/// URI. They MUST NOT treat this provider identifier as a second resource
/// identity.
public var instanceId: String
- /// Owning extension/provider identifier.
+ /// Owning declaration namespace.
public var extensionId: String
- /// Owning extension display name, when available.
+ /// Owning namespace title, when available.
public var extensionName: String?
/// Provider-local canvas type identifier.
public var canvasId: String
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
index 80fe05dbb..2e2ae11bf 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
@@ -1039,6 +1039,7 @@ public let clientDispatchableActions: Set = [
"chat/isReadChanged",
"chat/isArchivedChanged",
"chat/configChanged",
+ "chat/canvasRegistryRequested",
"chat/inputAnswerChanged",
"chat/inputCompleted",
"session/customizationToggled",
@@ -1061,6 +1062,7 @@ public func isClientDispatchable(_ action: StateAction) -> Bool {
.chatPendingMessageSet,
.chatPendingMessageRemoved, .chatQueuedMessagesReordered,
.chatIsReadChanged, .chatIsArchivedChanged, .chatConfigChanged,
+ .chatCanvasRegistryRequested,
.chatInputAnswerChanged, .chatInputCompleted,
.sessionCustomizationToggled,
.sessionMcpServerStartRequested, .sessionMcpServerStopRequested,
diff --git a/docs/guide/actions.md b/docs/guide/actions.md
index 7bd2daa84..b46243539 100644
--- a/docs/guide/actions.md
+++ b/docs/guide/actions.md
@@ -77,6 +77,14 @@ When a client dispatches an action, the server applies it to the state and also
| `chat/turnResume` | **Yes** | Resume the latest resumable errored turn without adding another message |
| `chat/truncated` | **Yes** | Turn history truncated (with optional `turnId` cutoff) |
+### Canvases (chat channel)
+
+| Type | Client-dispatchable? | When |
+|---|---|---|
+| `chat/canvasesChanged` | No | The chat's live Canvas channel references changed |
+| `chat/canvasRegistryRequested` | **Yes** | A client asks the host to materialize the Canvas type registry |
+| `chat/canvasRegistryChanged` | No | The host replaces or clears the authoritative Canvas type registry |
+
### Tool Calls (chat channel)
Tool calls follow a discriminated-union state machine — see [State Model — Tool Call Lifecycle](/guide/state-model#tool-call-lifecycle) for the full diagram.
diff --git a/docs/specification/subscriptions.md b/docs/specification/subscriptions.md
index 644c14bde..3178df30d 100644
--- a/docs/specification/subscriptions.md
+++ b/docs/specification/subscriptions.md
@@ -25,7 +25,7 @@ All channels use the same `subscribe` and `unsubscribe` messages.
| `ahp-automations://` | `AutomationState` | All automations visible to the client. This channel exists when `InitializeResult.automations` is present. |
| `ahp-session:/` | `SessionState` | Session metadata and the session's list of chats. |
| `ahp-chat:/` | `ChatState` | One chat: turns, streamed responses, tool calls, pending messages, input requests, and changesets. See [Chat Channel](/specification/chat-channel). |
-| `ahp-canvas:/` | `CanvasState` | Experimental. One canvas. The URI is listed in `ChatState.canvases`. See [Canvas Channel](/reference/canvas). |
+| `ahp-canvas:/` | `CanvasState` | Experimental. One canvas. The URI is listed in `ChatState.canvases` and is treated as an opaque resource identity. See [Canvas Channel](/reference/canvas). |
| `ahp-terminal:/` | `TerminalState` | One terminal. See [Terminal Channel](/specification/terminal-channel). |
| `ahp-tcp:/` | `EmptyState` | Experimental. One TCP connection that the host opened with `createTcpConnection`. Private to the client that created it. See [TCP Channel](/specification/tcp-channel). |
| `ahp-changeset:/` | `ChangesetState` | One changeset. The client builds the URI from the `Changeset.uriTemplate` of a session or chat. |
@@ -54,6 +54,7 @@ route any message using only its `method` and `params.channel`.
| `fetchTurns`, `completions`, `disposeChat` | The chat URI |
| `moveChat` | The URI of the chat to move |
| `createTerminal`, `disposeTerminal` | The terminal URI. For `createTerminal`, this is the URI of the new terminal. |
+| `openCanvas`, `disposeCanvas` | The canvas URI. For `openCanvas`, this is the URI of the canvas to open. |
| `createTcpConnection` | The URI of the new TCP channel |
| `invokeChangesetOperation` | The changeset URI |
| `subscribe`, `unsubscribe` | The URI of the channel being subscribed to or released |
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index 3aa29625b..96c43bcad 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -5168,19 +5168,15 @@
},
"CanvasExtensionDeclaration": {
"type": "object",
- "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
+ "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
"properties": {
"extensionId": {
"type": "string",
- "description": "Stable opaque identifier of the extension/provider."
- },
- "extensionSource": {
- "$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the extension/provider."
+ "description": "Stable opaque declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension/provider name, when available."
+ "description": "Human-readable namespace title, when available."
},
"canvases": {
"type": "array",
@@ -5192,21 +5188,20 @@
},
"required": [
"extensionId",
- "extensionSource",
"canvases"
]
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "displayName": {
+ "title": {
"type": "string",
- "description": "Human-readable canvas type name."
+ "description": "Human-readable canvas type title."
},
"description": {
"type": "string",
@@ -5219,21 +5214,21 @@
},
"required": [
"canvasId",
- "displayName",
+ "title",
"description",
"requiresInput"
]
},
"CanvasRegistry": {
"type": "object",
- "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "description": "Canvas types currently registered for one chat.",
"properties": {
"extensions": {
"type": "array",
"items": {
"$ref": "#/$defs/CanvasExtensionDeclaration"
},
- "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
}
},
"required": [
@@ -5246,7 +5241,7 @@
"properties": {
"resource": {
"$ref": "#/$defs/URI",
- "description": "Canonical Canvas channel URI. Subscribe to this resource for the full\nstate. It uses the `ahp-canvas:/[/...]` form documented\nby `openCanvas`."
+ "description": "Canvas channel URI. Subscribe to this resource for the full state."
}
},
"required": [
@@ -5263,11 +5258,11 @@
},
"extensionId": {
"type": "string",
- "description": "Owning extension/provider identifier."
+ "description": "Owning declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Owning extension display name, when available."
+ "description": "Owning namespace title, when available."
},
"canvasId": {
"type": "string",
@@ -7154,17 +7149,6 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
- "CanvasExtensionSource": {
- "enum": [
- "user",
- "project",
- "session",
- "plugin",
- "unknown"
- ],
- "type": "string",
- "description": "Where the runtime discovered an extension that declares a canvas type."
- },
"TerminalClaim": {
"oneOf": [
{
diff --git a/schema/state.schema.json b/schema/state.schema.json
index bbbe346fa..3e0b3b714 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -4777,19 +4777,15 @@
},
"CanvasExtensionDeclaration": {
"type": "object",
- "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
+ "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
"properties": {
"extensionId": {
"type": "string",
- "description": "Stable opaque identifier of the extension/provider."
- },
- "extensionSource": {
- "$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the extension/provider."
+ "description": "Stable opaque declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension/provider name, when available."
+ "description": "Human-readable namespace title, when available."
},
"canvases": {
"type": "array",
@@ -4801,21 +4797,20 @@
},
"required": [
"extensionId",
- "extensionSource",
"canvases"
]
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "displayName": {
+ "title": {
"type": "string",
- "description": "Human-readable canvas type name."
+ "description": "Human-readable canvas type title."
},
"description": {
"type": "string",
@@ -4828,21 +4823,21 @@
},
"required": [
"canvasId",
- "displayName",
+ "title",
"description",
"requiresInput"
]
},
"CanvasRegistry": {
"type": "object",
- "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "description": "Canvas types currently registered for one chat.",
"properties": {
"extensions": {
"type": "array",
"items": {
"$ref": "#/$defs/CanvasExtensionDeclaration"
},
- "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
}
},
"required": [
@@ -4855,7 +4850,7 @@
"properties": {
"resource": {
"$ref": "#/$defs/URI",
- "description": "Canonical Canvas channel URI. Subscribe to this resource for the full\nstate. It uses the `ahp-canvas:/[/...]` form documented\nby `openCanvas`."
+ "description": "Canvas channel URI. Subscribe to this resource for the full state."
}
},
"required": [
@@ -4872,11 +4867,11 @@
},
"extensionId": {
"type": "string",
- "description": "Owning extension/provider identifier."
+ "description": "Owning declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Owning extension display name, when available."
+ "description": "Owning namespace title, when available."
},
"canvasId": {
"type": "string",
@@ -6841,17 +6836,6 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
- "CanvasExtensionSource": {
- "enum": [
- "user",
- "project",
- "session",
- "plugin",
- "unknown"
- ],
- "type": "string",
- "description": "Where the runtime discovered an extension that declares a canvas type."
- },
"ChangesetStatus": {
"enum": [
"computing",
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index 4717edf41..fd7e995d5 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -2359,6 +2359,12 @@ object AhpCommands {
fun disposeTerminal(id: Long, params: DisposeTerminalParams): JsonRpcRequest =
JsonRpcRequest(id = id, method = "disposeTerminal", params = params)
+ fun openCanvas(id: Long, params: OpenCanvasParams): JsonRpcRequest =
+ JsonRpcRequest(id = id, method = "openCanvas", params = params)
+
+ fun disposeCanvas(id: Long, params: DisposeCanvasParams): JsonRpcRequest =
+ JsonRpcRequest(id = id, method = "disposeCanvas", params = params)
+
fun resolveSessionConfig(id: Long, params: ResolveSessionConfigParams): JsonRpcRequest =
JsonRpcRequest(id = id, method = "resolveSessionConfig", params = params)
diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts
index 5235108a6..acf2b6e71 100644
--- a/scripts/generate-markdown.ts
+++ b/scripts/generate-markdown.ts
@@ -602,6 +602,10 @@ function renderActionInterfaceBlock(iface: InterfaceDeclaration): string {
}
const heading = `### \`${labelBase}${variantSuffix}\``;
lines.push(heading + '\n');
+ const stability = readStability(iface);
+ if (stability) {
+ lines.push(`\n`);
+ }
const isClientDispatchable = hasJsDocTag(iface, 'clientDispatchable');
const desc = getJsDocDescription(iface);
const prefix = isClientDispatchable ? '**Client-dispatchable.** ' : '';
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index 506c9c73e..680300a7e 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -2290,6 +2290,14 @@ public enum AHPCommands {
public static func disposeTerminal(id: Int, params: DisposeTerminalParams) -> JsonRpcRequest {
JsonRpcRequest(id: id, method: "disposeTerminal", params: params)
}
+
+ public static func openCanvas(id: Int, params: OpenCanvasParams) -> JsonRpcRequest {
+ JsonRpcRequest(id: id, method: "openCanvas", params: params)
+ }
+
+ public static func disposeCanvas(id: Int, params: DisposeCanvasParams) -> JsonRpcRequest {
+ JsonRpcRequest(id: id, method: "disposeCanvas", params: params)
+ }
}
/// Typed helper for constructing client → server notifications.
diff --git a/scripts/read-stability.test.ts b/scripts/read-stability.test.ts
index 72eb0554a..64e8c6ae3 100644
--- a/scripts/read-stability.test.ts
+++ b/scripts/read-stability.test.ts
@@ -70,7 +70,7 @@ test('invalid, missing, and repeated stability values fail explicitly', () => {
assert.equal(getDocumentation(ordinary), '');
});
-test('API docs show stability on entrypoint fields and notification methods only', () => {
+test('API docs show stability on entrypoint fields and experimental actions', () => {
const out = mkdtempSync(join(tmpdir(), 'ahp-stability-'));
try {
const project = new Project({ tsConfigFilePath: resolve(root, 'types/tsconfig.json') });
@@ -99,6 +99,13 @@ test('API docs show stability on entrypoint fields and notification methods only
assert.ok(!common.includes('### `reconnect`\n\n'));
+ const chat = readFileSync(join(out, 'chat.md'), 'utf8');
+ for (const action of ['chat/canvasRegistryRequested', 'chat/canvasRegistryChanged']) {
+ assert.ok(chat.includes(`### \`${action}\`\n\n`));
+ }
+ for (const action of ['chat/turnStarted', 'chat/delta']) {
+ assert.ok(!chat.includes(`### \`${action}\`\n\n[/...]`: the lowercase scheme, no authority,
- * query, or fragment, and one or more non-empty path segments containing only
- * URI-unreserved characters (`A-Z a-z 0-9 - . _ ~`). Servers MUST reject
- * non-canonical channels so URI parsing cannot change the resource identity.
- *
- * The null result is not presentation state. Clients MUST observe
+ * Clients MUST observe
* {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
* authoritative membership, availability, and presentation state.
*
- * Clients MUST NOT call this command unless the selected agent advertises
+ * Clients MUST NOT call this command unless the chat's agent advertises
* {@link AgentCapabilities.canvases}.
*
* @category Commands
@@ -43,8 +36,8 @@ import type { ChatState } from '../channels-chat/state.js';
*/
export interface OpenCanvasParams extends BaseParams {
/**
- * Client-chosen canonical URI for the canvas resource, for example
- * `ahp-canvas:/`. See the command-level canonical-form requirement.
+ * Client-chosen URI for the canvas resource, for example
+ * `ahp-canvas:/`.
*/
channel: URI;
/** Exact chat URI that owns the canvas instance. */
@@ -61,12 +54,10 @@ export interface OpenCanvasParams extends BaseParams {
* Closes and disposes one live canvas resource.
*
* The server removes the resource from the owning chat's
- * {@link ChatState.canvases} references and notifies the provider that owns the
- * instance. Repeating this command after the resource is gone is a no-op.
- * The inherited `channel` MUST use the canonical Canvas URI form documented by
- * {@link OpenCanvasParams}.
+ * {@link ChatState.canvases} references. Repeating this command after the
+ * resource is gone is a no-op.
*
- * Clients MUST NOT call this command unless the selected agent advertises
+ * Clients MUST NOT call this command unless the chat's agent advertises
* {@link AgentCapabilities.canvases}.
*
* @category Commands
@@ -76,4 +67,7 @@ export interface OpenCanvasParams extends BaseParams {
* @version 1
* @stability 1.0
*/
-export interface DisposeCanvasParams extends BaseParams {}
+export interface DisposeCanvasParams extends BaseParams {
+ /** URI of the canvas resource to dispose. */
+ channel: URI;
+}
diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts
index b3c089c17..6a4bd0f57 100644
--- a/types/channels-canvas/state.ts
+++ b/types/channels-canvas/state.ts
@@ -10,39 +10,17 @@
import type { URI } from '../common/state.js';
/**
- * Where the runtime discovered an extension that declares a canvas type.
+ * One namespace that declares canvas types for a chat.
*
- * @category Canvas State
- * @nonexhaustive
- */
-export const enum CanvasExtensionSource {
- /** Extension installed for the current user. */
- User = 'user',
- /** Extension discovered in the current project. */
- Project = 'project',
- /** Extension installed only for the current session. */
- Session = 'session',
- /** Extension contributed by an installed plugin. */
- Plugin = 'plugin',
- /** Extension source is unavailable or not recognized by this protocol version. */
- Unspecified = 'unknown',
-}
-
-/**
- * One extension/provider that declares canvas types for an exact chat.
- *
- * The extension metadata is normalized here instead of repeated on every
- * {@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT
- * infer its source or management behavior by parsing it.
+ * `extensionId` is opaque. Clients MUST NOT infer management behavior by
+ * parsing it.
*
* @category Canvas State
*/
export interface CanvasExtensionDeclaration {
- /** Stable opaque identifier of the extension/provider. */
+ /** Stable opaque declaration namespace. */
extensionId: string;
- /** Where the runtime discovered the extension/provider. */
- extensionSource: CanvasExtensionSource;
- /** Human-readable extension/provider name, when available. */
+ /** Human-readable namespace title, when available. */
extensionName?: string;
/**
* Canvas types declared by this extension/provider. `canvasId` MUST be
@@ -54,10 +32,6 @@ export interface CanvasExtensionDeclaration {
/**
* One canvas type currently available to open.
*
- * This is discovery metadata, not a live canvas instance. Live instances are
- * represented by {@link CanvasReference} entries on chat state and their
- * corresponding {@link CanvasState} channels.
- *
* This experimental object may gain optional discovery metadata. Clients
* SHOULD ignore fields they do not understand.
*
@@ -66,8 +40,8 @@ export interface CanvasExtensionDeclaration {
export interface CanvasTypeDeclaration {
/** Provider-local canvas type identifier. */
canvasId: string;
- /** Human-readable canvas type name. */
- displayName: string;
+ /** Human-readable canvas type title. */
+ title: string;
/** Description of what the canvas type does. */
description: string;
/** Whether opening this canvas requires an input flow. */
@@ -75,17 +49,13 @@ export interface CanvasTypeDeclaration {
}
/**
- * Canvas types currently registered for one exact chat.
- *
- * This is a compact presentation registry, not extension installation state
- * and not the list of live instances. The host replaces the complete registry
- * through `chat/canvasRegistryChanged` whenever provider registration changes.
+ * Canvas types currently registered for one chat.
*
* @category Canvas State
*/
export interface CanvasRegistry {
/**
- * Declaring extensions/providers. `extensionId` MUST be unique within this
+ * Declaring namespaces. `extensionId` MUST be unique within this
* array. An empty array is an authoritative loaded registry with no types.
*/
extensions: CanvasExtensionDeclaration[];
@@ -102,9 +72,7 @@ export interface CanvasRegistry {
*/
export interface CanvasReference {
/**
- * Canonical Canvas channel URI. Subscribe to this resource for the full
- * state. It uses the `ahp-canvas:/[/...]` form documented
- * by `openCanvas`.
+ * Canvas channel URI. Subscribe to this resource for the full state.
*/
resource: URI;
}
@@ -127,9 +95,9 @@ export interface CanvasState {
* identity.
*/
instanceId: string;
- /** Owning extension/provider identifier. */
+ /** Owning declaration namespace. */
extensionId: string;
- /** Owning extension display name, when available. */
+ /** Owning namespace title, when available. */
extensionName?: string;
/** Provider-local canvas type identifier. */
canvasId: string;
diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts
index 6f7b93cc8..121827962 100644
--- a/types/channels-chat/actions.ts
+++ b/types/channels-chat/actions.ts
@@ -64,7 +64,6 @@ interface ToolCallActionBase {
*
* @category Chat Actions
* @version 1
- * @stability 1.0
* @clientDispatchable
*/
export interface ChatTurnStartedAction {
@@ -100,7 +99,6 @@ export interface ChatTurnStartedAction {
*
* @category Chat Actions
* @version 1
- * @stability 1.0
*/
export interface ChatDeltaAction {
type: ActionType.ChatDelta;
@@ -630,22 +628,21 @@ export interface ChatCanvasesChangedAction {
}
/**
- * Requests materialization of the exact chat's Canvas type registry.
+ * Requests materialization of the chat's Canvas type registry.
*
- * This operation is potentially effectful: for a persisted cold chat, the host
- * may resume its backing runtime, discover extensions, and execute extension
- * startup code. It MUST NOT create a backing for a brand-new chat, send a user
- * message, or invoke a model.
+ * A host MUST reject the action when the chat's agent does not advertise
+ * {@link AgentCapabilities.canvases} or when the chat cannot materialize the
+ * registry. Materialization MUST NOT send a user message or invoke a model.
*
* The reducer is a no-op. The host remains authoritative and publishes the
* complete result through
* {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
* Clients do not need to dispatch this action when
- * {@link ChatState.canvasRegistry} is already present because live provider
- * registration changes are published reactively.
+ * {@link ChatState.canvasRegistry} is already present.
*
* @category Chat Actions
* @version 1
+ * @stability 1.0
* @clientDispatchable
*/
export interface ChatCanvasRegistryRequestedAction {
@@ -653,13 +650,14 @@ export interface ChatCanvasRegistryRequestedAction {
}
/**
- * The Canvas type registry for this exact chat changed.
+ * The Canvas type registry for this chat changed.
*
* Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
* Set to `undefined` when the registry is no longer materialized or available.
*
* @category Chat Actions
* @version 1
+ * @stability 1.0
*/
export interface ChatCanvasRegistryChangedAction {
type: ActionType.ChatCanvasRegistryChanged;
diff --git a/types/test-cases/reducers/293-chat-canvasregistryrequested-noop.json b/types/test-cases/reducers/327-chat-canvasregistryrequested-noop.json
similarity index 100%
rename from types/test-cases/reducers/293-chat-canvasregistryrequested-noop.json
rename to types/test-cases/reducers/327-chat-canvasregistryrequested-noop.json
diff --git a/types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json b/types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json
similarity index 88%
rename from types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json
rename to types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json
index 5386b3501..8b95af49d 100644
--- a/types/test-cases/reducers/294-chat-canvasregistrychanged-sets-registry.json
+++ b/types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json
@@ -15,12 +15,11 @@
"extensions": [
{
"extensionId": "opaque-provider-id",
- "extensionSource": "project",
"extensionName": "Project Canvases",
"canvases": [
{
"canvasId": "diagram",
- "displayName": "Diagram",
+ "title": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
}
@@ -40,12 +39,11 @@
"extensions": [
{
"extensionId": "opaque-provider-id",
- "extensionSource": "project",
"extensionName": "Project Canvases",
"canvases": [
{
"canvasId": "diagram",
- "displayName": "Diagram",
+ "title": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
}
diff --git a/types/test-cases/reducers/295-chat-canvasregistrychanged-clears-registry.json b/types/test-cases/reducers/329-chat-canvasregistrychanged-clears-registry.json
similarity index 100%
rename from types/test-cases/reducers/295-chat-canvasregistrychanged-clears-registry.json
rename to types/test-cases/reducers/329-chat-canvasregistrychanged-clears-registry.json
diff --git a/types/test-cases/round-trips/076-chat-canvas-registry.json b/types/test-cases/round-trips/086-chat-canvas-registry.json
similarity index 88%
rename from types/test-cases/round-trips/076-chat-canvas-registry.json
rename to types/test-cases/round-trips/086-chat-canvas-registry.json
index 1eb04e89b..b4994940b 100644
--- a/types/test-cases/round-trips/076-chat-canvas-registry.json
+++ b/types/test-cases/round-trips/086-chat-canvas-registry.json
@@ -15,18 +15,17 @@
"extensions": [
{
"extensionId": "opaque-provider-id",
- "extensionSource": "plugin",
"extensionName": "Diagram Tools",
"canvases": [
{
"canvasId": "diagram",
- "displayName": "Diagram",
+ "title": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
},
{
"canvasId": "form",
- "displayName": "Form",
+ "title": "Form",
"description": "Collect structured input.",
"requiresInput": true
}
@@ -50,18 +49,17 @@
"extensions": [
{
"extensionId": "opaque-provider-id",
- "extensionSource": "plugin",
"extensionName": "Diagram Tools",
"canvases": [
{
"canvasId": "diagram",
- "displayName": "Diagram",
+ "title": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
},
{
"canvasId": "form",
- "displayName": "Form",
+ "title": "Form",
"description": "Collect structured input.",
"requiresInput": true
}
diff --git a/types/test-cases/round-trips/077-chat-canvas-registry-requested-action.json b/types/test-cases/round-trips/087-chat-canvas-registry-requested-action.json
similarity index 100%
rename from types/test-cases/round-trips/077-chat-canvas-registry-requested-action.json
rename to types/test-cases/round-trips/087-chat-canvas-registry-requested-action.json
diff --git a/types/test-cases/round-trips/078-chat-canvas-registry-changed-action.json b/types/test-cases/round-trips/088-chat-canvas-registry-changed-action.json
similarity index 100%
rename from types/test-cases/round-trips/078-chat-canvas-registry-changed-action.json
rename to types/test-cases/round-trips/088-chat-canvas-registry-changed-action.json
diff --git a/types/test-cases/round-trips/079-open-canvas-client-addressed-resource.json b/types/test-cases/round-trips/089-open-canvas-client-addressed-resource.json
similarity index 100%
rename from types/test-cases/round-trips/079-open-canvas-client-addressed-resource.json
rename to types/test-cases/round-trips/089-open-canvas-client-addressed-resource.json
diff --git a/types/test-cases/round-trips/080-dispose-canvas-client-addressed-resource.json b/types/test-cases/round-trips/090-dispose-canvas-client-addressed-resource.json
similarity index 100%
rename from types/test-cases/round-trips/080-dispose-canvas-client-addressed-resource.json
rename to types/test-cases/round-trips/090-dispose-canvas-client-addressed-resource.json
diff --git a/types/test-cases/round-trips/091-agent-canvas-capability.json b/types/test-cases/round-trips/091-agent-canvas-capability.json
new file mode 100644
index 000000000..bcd0c8d66
--- /dev/null
+++ b/types/test-cases/round-trips/091-agent-canvas-capability.json
@@ -0,0 +1,54 @@
+{
+ "name": "agent-canvas-capability",
+ "group": "A",
+ "description": "AgentInfo preserves the presence of the empty canvases capability object.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "1.1.0",
+ "serverSeq": 3,
+ "snapshots": [
+ {
+ "resource": "ahp-root://",
+ "state": {
+ "agents": [
+ {
+ "provider": "example",
+ "displayName": "Example",
+ "description": "Example agent",
+ "models": [],
+ "capabilities": {
+ "canvases": {}
+ }
+ }
+ ]
+ },
+ "fromSeq": 3
+ }
+ ]
+ },
+ "acceptableOutputs": [
+ {
+ "protocolVersion": "1.1.0",
+ "serverSeq": 3,
+ "snapshots": [
+ {
+ "resource": "ahp-root://",
+ "state": {
+ "agents": [
+ {
+ "provider": "example",
+ "displayName": "Example",
+ "description": "Example agent",
+ "models": [],
+ "capabilities": {
+ "canvases": {}
+ }
+ }
+ ]
+ },
+ "fromSeq": 3
+ }
+ ]
+ }
+ ]
+}
From b58d505e4f068edf0939c19f7acd65b580cb88cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Wed, 4 Nov 2026 22:56:03 -0800
Subject: [PATCH 10/12] canvas: regenerate after main rebase
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Generated/Actions.generated.cs | 20 ++++---
clients/go/ahptypes/actions.generated.go | 18 +++---
clients/rust/crates/ahp-types/src/actions.rs | 18 +++---
schema/actions.schema.json | 54 +++++++----------
schema/commands.schema.json | 60 ++++++++-----------
schema/errors.schema.json | 60 ++++++++-----------
6 files changed, 100 insertions(+), 130 deletions(-)
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
index 37ebeac23..512cfbbb2 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
@@ -1851,28 +1851,30 @@ public sealed record ChatCanvasesChangedAction
public List? Canvases { get; init; }
}
-/// Requests materialization of the exact chat's Canvas type registry.
+/// Requests materialization of the chat's Canvas type registry.
///
-/// This operation is potentially effectful: for a persisted cold chat, the host
-/// may resume its backing runtime, discover extensions, and execute extension
-/// startup code. It MUST NOT create a backing for a brand-new chat, send a user
-/// message, or invoke a model.
+/// A host MUST reject the action when the chat's agent does not advertise
+/// {@link AgentCapabilities.canvases} or when the chat cannot materialize the
+/// registry. Materialization MUST NOT send a user message or invoke a model.
///
/// The reducer is a no-op. The host remains authoritative and publishes the
/// complete result through
/// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
/// Clients do not need to dispatch this action when
-/// {@link ChatState.canvasRegistry} is already present because live provider
-/// registration changes are published reactively.
+/// {@link ChatState.canvasRegistry} is already present.
+///
+/// Stability: 1.0 - Early development.
public sealed record ChatCanvasRegistryRequestedAction
{
public ActionType Type { get; init; } = ActionType.ChatCanvasRegistryRequested;
}
-/// The Canvas type registry for this exact chat changed.
+/// The Canvas type registry for this chat changed.
///
/// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
-/// Set to `undefined` when the registry is no longer materialized or available.
+/// Set to `undefined` when the registry is no longer materialized or available.
+///
+/// Stability: 1.0 - Early development.
public sealed record ChatCanvasRegistryChangedAction
{
public ActionType Type { get; init; } = ActionType.ChatCanvasRegistryChanged;
diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go
index 255b7a08d..6b3f06078 100644
--- a/clients/go/ahptypes/actions.generated.go
+++ b/clients/go/ahptypes/actions.generated.go
@@ -723,27 +723,29 @@ type ChatCanvasesChangedAction struct {
Canvases []CanvasReference `json:"canvases,omitempty"`
}
-// Requests materialization of the exact chat's Canvas type registry.
+// Requests materialization of the chat's Canvas type registry.
//
-// This operation is potentially effectful: for a persisted cold chat, the host
-// may resume its backing runtime, discover extensions, and execute extension
-// startup code. It MUST NOT create a backing for a brand-new chat, send a user
-// message, or invoke a model.
+// A host MUST reject the action when the chat's agent does not advertise
+// {@link AgentCapabilities.canvases} or when the chat cannot materialize the
+// registry. Materialization MUST NOT send a user message or invoke a model.
//
// The reducer is a no-op. The host remains authoritative and publishes the
// complete result through
// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
// Clients do not need to dispatch this action when
-// {@link ChatState.canvasRegistry} is already present because live provider
-// registration changes are published reactively.
+// {@link ChatState.canvasRegistry} is already present.
+//
+// Stability: 1.0 - Early development.
type ChatCanvasRegistryRequestedAction struct {
Type ActionType `json:"type"`
}
-// The Canvas type registry for this exact chat changed.
+// The Canvas type registry for this chat changed.
//
// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
// Set to `undefined` when the registry is no longer materialized or available.
+//
+// Stability: 1.0 - Early development.
type ChatCanvasRegistryChangedAction struct {
Type ActionType `json:"type"`
// Authoritative registry, or `undefined` to clear it.
diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs
index 1a1fc7966..55b67d64f 100644
--- a/clients/rust/crates/ahp-types/src/actions.rs
+++ b/clients/rust/crates/ahp-types/src/actions.rs
@@ -1173,27 +1173,29 @@ pub struct ChatCanvasesChangedAction {
pub canvases: Option>,
}
-/// Requests materialization of the exact chat's Canvas type registry.
+/// Requests materialization of the chat's Canvas type registry.
///
-/// This operation is potentially effectful: for a persisted cold chat, the host
-/// may resume its backing runtime, discover extensions, and execute extension
-/// startup code. It MUST NOT create a backing for a brand-new chat, send a user
-/// message, or invoke a model.
+/// A host MUST reject the action when the chat's agent does not advertise
+/// {@link AgentCapabilities.canvases} or when the chat cannot materialize the
+/// registry. Materialization MUST NOT send a user message or invoke a model.
///
/// The reducer is a no-op. The host remains authoritative and publishes the
/// complete result through
/// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
/// Clients do not need to dispatch this action when
-/// {@link ChatState.canvasRegistry} is already present because live provider
-/// registration changes are published reactively.
+/// {@link ChatState.canvasRegistry} is already present.
+///
+/// Stability: 1.0 - Early development.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatCanvasRegistryRequestedAction {}
-/// The Canvas type registry for this exact chat changed.
+/// The Canvas type registry for this chat changed.
///
/// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
/// Set to `undefined` when the registry is no longer materialized or available.
+///
+/// Stability: 1.0 - Early development.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ChatCanvasRegistryChangedAction {
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index 5ef63b516..0335f81d5 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -1462,7 +1462,7 @@
},
"ChatCanvasRegistryRequestedAction": {
"type": "object",
- "description": "Requests materialization of the exact chat's Canvas type registry.\n\nThis operation is potentially effectful: for a persisted cold chat, the host\nmay resume its backing runtime, discover extensions, and execute extension\nstartup code. It MUST NOT create a backing for a brand-new chat, send a user\nmessage, or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present because live provider\nregistration changes are published reactively.",
+ "description": "Requests materialization of the chat's Canvas type registry.\n\nA host MUST reject the action when the chat's agent does not advertise\n{@link AgentCapabilities.canvases} or when the chat cannot materialize the\nregistry. Materialization MUST NOT send a user message or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present.\n\nStability: 1.0 - Early development.",
"properties": {
"type": {
"const": "chat/canvasRegistryRequested"
@@ -1474,7 +1474,7 @@
},
"ChatCanvasRegistryChangedAction": {
"type": "object",
- "description": "The Canvas type registry for this exact chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.",
+ "description": "The Canvas type registry for this chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.\n\nStability: 1.0 - Early development.",
"properties": {
"type": {
"const": "chat/canvasRegistryChanged"
@@ -3367,9 +3367,8 @@
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
"canvases": {
- "type": "object",
- "additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
+ "$ref": "#/$defs/CanvasesCapability",
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references.\n\nStability: 1.0 - Early development."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -3381,6 +3380,11 @@
}
}
},
+ "CanvasesCapability": {
+ "type": "object",
+ "description": "Presence capability for the experimental Canvas registry and lifecycle\ncommands.\n\nStability: 1.0 - Early development.",
+ "properties": {}
+ },
"MultipleChatsCapability": {
"type": "object",
"description": "Options for the {@link AgentCapabilities.multipleChats} capability.",
@@ -5331,7 +5335,7 @@
},
"canvasRegistry": {
"$ref": "#/$defs/CanvasRegistry",
- "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types.\n\nStability: 1.0 - Early development."
},
"canvases": {
"type": "array",
@@ -7516,19 +7520,15 @@
},
"CanvasExtensionDeclaration": {
"type": "object",
- "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
+ "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
"properties": {
"extensionId": {
"type": "string",
- "description": "Stable opaque identifier of the extension/provider."
- },
- "extensionSource": {
- "$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the extension/provider."
+ "description": "Stable opaque declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension/provider name, when available."
+ "description": "Human-readable namespace title, when available."
},
"canvases": {
"type": "array",
@@ -7540,21 +7540,20 @@
},
"required": [
"extensionId",
- "extensionSource",
"canvases"
]
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "displayName": {
+ "title": {
"type": "string",
- "description": "Human-readable canvas type name."
+ "description": "Human-readable canvas type title."
},
"description": {
"type": "string",
@@ -7567,21 +7566,21 @@
},
"required": [
"canvasId",
- "displayName",
+ "title",
"description",
"requiresInput"
]
},
"CanvasRegistry": {
"type": "object",
- "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "description": "Canvas types currently registered for one chat.",
"properties": {
"extensions": {
"type": "array",
"items": {
"$ref": "#/$defs/CanvasExtensionDeclaration"
},
- "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
}
},
"required": [
@@ -7611,11 +7610,11 @@
},
"extensionId": {
"type": "string",
- "description": "Owning extension/provider identifier."
+ "description": "Owning declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Owning extension display name, when available."
+ "description": "Owning namespace title, when available."
},
"canvasId": {
"type": "string",
@@ -9938,17 +9937,6 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
- "CanvasExtensionSource": {
- "enum": [
- "user",
- "project",
- "session",
- "plugin",
- "unknown"
- ],
- "type": "string",
- "description": "Where the runtime discovered an extension that declares a canvas type."
- },
"ChangesetOperationScope": {
"enum": [
"changeset",
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index 9dd30d634..7950025d5 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -1578,7 +1578,7 @@
},
"OpenCanvasParams": {
"type": "object",
- "description": "Opens or focuses one client-addressed canvas resource in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` focuses or refreshes that\nlogical instance rather than creating a duplicate. Reusing the channel for\na different chat, extension, or canvas type MUST fail with `Conflict`.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Opens one client-addressed canvas resource in a chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` is idempotent. Reusing it for\na different chat, extension, or canvas type MUST fail with `Conflict`.\nClients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -1611,11 +1611,11 @@
},
"DisposeCanvasParams": {
"type": "object",
- "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references and notifies the provider that owns the\ninstance. Repeating this command after the resource is gone is a no-op.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references. Repeating this command after the\nresource is gone is a no-op.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
- "description": "Channel URI this command targets."
+ "description": "URI of the canvas resource to dispose."
},
"_meta": {
"type": "object",
@@ -2690,9 +2690,8 @@
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
"canvases": {
- "type": "object",
- "additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
+ "$ref": "#/$defs/CanvasesCapability",
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references.\n\nStability: 1.0 - Early development."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -2704,6 +2703,11 @@
}
}
},
+ "CanvasesCapability": {
+ "type": "object",
+ "description": "Presence capability for the experimental Canvas registry and lifecycle\ncommands.\n\nStability: 1.0 - Early development.",
+ "properties": {}
+ },
"MultipleChatsCapability": {
"type": "object",
"description": "Options for the {@link AgentCapabilities.multipleChats} capability.",
@@ -4654,7 +4658,7 @@
},
"canvasRegistry": {
"$ref": "#/$defs/CanvasRegistry",
- "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types.\n\nStability: 1.0 - Early development."
},
"canvases": {
"type": "array",
@@ -6839,19 +6843,15 @@
},
"CanvasExtensionDeclaration": {
"type": "object",
- "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
+ "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
"properties": {
"extensionId": {
"type": "string",
- "description": "Stable opaque identifier of the extension/provider."
- },
- "extensionSource": {
- "$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the extension/provider."
+ "description": "Stable opaque declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension/provider name, when available."
+ "description": "Human-readable namespace title, when available."
},
"canvases": {
"type": "array",
@@ -6863,21 +6863,20 @@
},
"required": [
"extensionId",
- "extensionSource",
"canvases"
]
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "displayName": {
+ "title": {
"type": "string",
- "description": "Human-readable canvas type name."
+ "description": "Human-readable canvas type title."
},
"description": {
"type": "string",
@@ -6890,21 +6889,21 @@
},
"required": [
"canvasId",
- "displayName",
+ "title",
"description",
"requiresInput"
]
},
"CanvasRegistry": {
"type": "object",
- "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "description": "Canvas types currently registered for one chat.",
"properties": {
"extensions": {
"type": "array",
"items": {
"$ref": "#/$defs/CanvasExtensionDeclaration"
},
- "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
}
},
"required": [
@@ -6934,11 +6933,11 @@
},
"extensionId": {
"type": "string",
- "description": "Owning extension/provider identifier."
+ "description": "Owning declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Owning extension display name, when available."
+ "description": "Owning namespace title, when available."
},
"canvasId": {
"type": "string",
@@ -9662,7 +9661,7 @@
},
"ChatCanvasRegistryRequestedAction": {
"type": "object",
- "description": "Requests materialization of the exact chat's Canvas type registry.\n\nThis operation is potentially effectful: for a persisted cold chat, the host\nmay resume its backing runtime, discover extensions, and execute extension\nstartup code. It MUST NOT create a backing for a brand-new chat, send a user\nmessage, or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present because live provider\nregistration changes are published reactively.",
+ "description": "Requests materialization of the chat's Canvas type registry.\n\nA host MUST reject the action when the chat's agent does not advertise\n{@link AgentCapabilities.canvases} or when the chat cannot materialize the\nregistry. Materialization MUST NOT send a user message or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present.\n\nStability: 1.0 - Early development.",
"properties": {
"type": {
"const": "chat/canvasRegistryRequested"
@@ -9674,7 +9673,7 @@
},
"ChatCanvasRegistryChangedAction": {
"type": "object",
- "description": "The Canvas type registry for this exact chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.",
+ "description": "The Canvas type registry for this chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.\n\nStability: 1.0 - Early development.",
"properties": {
"type": {
"const": "chat/canvasRegistryChanged"
@@ -11807,17 +11806,6 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
- "CanvasExtensionSource": {
- "enum": [
- "user",
- "project",
- "session",
- "plugin",
- "unknown"
- ],
- "type": "string",
- "description": "Where the runtime discovered an extension that declares a canvas type."
- },
"TerminalLifecycleState": {
"oneOf": [
{
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 820682ecb..4a6678623 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -713,9 +713,8 @@
"description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.",
"properties": {
"canvases": {
- "type": "object",
- "additionalProperties": {},
- "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references."
+ "$ref": "#/$defs/CanvasesCapability",
+ "description": "Experimental Canvas API.\n\nThe agent can materialize an exact chat's\n{@link ChatState.canvasRegistry | Canvas registry} through\n`chat/canvasRegistryRequested` and open a declared type with `openCanvas`.\nLive instances are published through the chat's canvas references and\ntheir `ahp-canvas:` state channels.\n\nClients MUST treat absence as unsupported and SHOULD tolerate additive\nregistry metadata while the Canvas channel remains experimental. This\ncapability does not gate server-published Canvas references.\n\nStability: 1.0 - Early development."
},
"multipleChats": {
"$ref": "#/$defs/MultipleChatsCapability",
@@ -727,6 +726,11 @@
}
}
},
+ "CanvasesCapability": {
+ "type": "object",
+ "description": "Presence capability for the experimental Canvas registry and lifecycle\ncommands.\n\nStability: 1.0 - Early development.",
+ "properties": {}
+ },
"MultipleChatsCapability": {
"type": "object",
"description": "Options for the {@link AgentCapabilities.multipleChats} capability.",
@@ -2677,7 +2681,7 @@
},
"canvasRegistry": {
"$ref": "#/$defs/CanvasRegistry",
- "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types."
+ "description": "Canvas types currently registered for this exact chat.\n\nAbsence means the registry has not been materialized or is not currently\navailable. Clients that need it MAY dispatch\n`chat/canvasRegistryRequested`; the host publishes the authoritative\nregistry through `chat/canvasRegistryChanged`. An empty\n{@link CanvasRegistry.extensions} array is a loaded registry with no\navailable canvas types.\n\nStability: 1.0 - Early development."
},
"canvases": {
"type": "array",
@@ -4862,19 +4866,15 @@
},
"CanvasExtensionDeclaration": {
"type": "object",
- "description": "One extension/provider that declares canvas types for an exact chat.\n\nThe extension metadata is normalized here instead of repeated on every\n{@link CanvasTypeDeclaration}. `extensionId` is opaque: clients MUST NOT\ninfer its source or management behavior by parsing it.",
+ "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
"properties": {
"extensionId": {
"type": "string",
- "description": "Stable opaque identifier of the extension/provider."
- },
- "extensionSource": {
- "$ref": "#/$defs/CanvasExtensionSource",
- "description": "Where the runtime discovered the extension/provider."
+ "description": "Stable opaque declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Human-readable extension/provider name, when available."
+ "description": "Human-readable namespace title, when available."
},
"canvases": {
"type": "array",
@@ -4886,21 +4886,20 @@
},
"required": [
"extensionId",
- "extensionSource",
"canvases"
]
},
"CanvasTypeDeclaration": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis is discovery metadata, not a live canvas instance. Live instances are\nrepresented by {@link CanvasReference} entries on chat state and their\ncorresponding {@link CanvasState} channels.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
"properties": {
"canvasId": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "displayName": {
+ "title": {
"type": "string",
- "description": "Human-readable canvas type name."
+ "description": "Human-readable canvas type title."
},
"description": {
"type": "string",
@@ -4913,21 +4912,21 @@
},
"required": [
"canvasId",
- "displayName",
+ "title",
"description",
"requiresInput"
]
},
"CanvasRegistry": {
"type": "object",
- "description": "Canvas types currently registered for one exact chat.\n\nThis is a compact presentation registry, not extension installation state\nand not the list of live instances. The host replaces the complete registry\nthrough `chat/canvasRegistryChanged` whenever provider registration changes.",
+ "description": "Canvas types currently registered for one chat.",
"properties": {
"extensions": {
"type": "array",
"items": {
"$ref": "#/$defs/CanvasExtensionDeclaration"
},
- "description": "Declaring extensions/providers. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
}
},
"required": [
@@ -4957,11 +4956,11 @@
},
"extensionId": {
"type": "string",
- "description": "Owning extension/provider identifier."
+ "description": "Owning declaration namespace."
},
"extensionName": {
"type": "string",
- "description": "Owning extension display name, when available."
+ "description": "Owning namespace title, when available."
},
"canvasId": {
"type": "string",
@@ -7801,7 +7800,7 @@
},
"OpenCanvasParams": {
"type": "object",
- "description": "Opens or focuses one client-addressed canvas resource in an exact chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` focuses or refreshes that\nlogical instance rather than creating a duplicate. Reusing the channel for\na different chat, extension, or canvas type MUST fail with `Conflict`.\n\nThe null result is not presentation state. Clients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Opens one client-addressed canvas resource in a chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` is idempotent. Reusing it for\na different chat, extension, or canvas type MUST fail with `Conflict`.\nClients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -7834,11 +7833,11 @@
},
"DisposeCanvasParams": {
"type": "object",
- "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references and notifies the provider that owns the\ninstance. Repeating this command after the resource is gone is a no-op.\n\nClients MUST NOT call this command unless the selected agent advertises\n{@link AgentCapabilities.canvases}.",
+ "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references. Repeating this command after the\nresource is gone is a no-op.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
- "description": "Channel URI this command targets."
+ "description": "URI of the canvas resource to dispose."
},
"_meta": {
"type": "object",
@@ -8810,17 +8809,6 @@
"type": "string",
"description": "Why a tool call was cancelled."
},
- "CanvasExtensionSource": {
- "enum": [
- "user",
- "project",
- "session",
- "plugin",
- "unknown"
- ],
- "type": "string",
- "description": "Where the runtime discovered an extension that declares a canvas type."
- },
"TerminalClaim": {
"oneOf": [
{
@@ -10761,7 +10749,7 @@
},
"ChatCanvasRegistryRequestedAction": {
"type": "object",
- "description": "Requests materialization of the exact chat's Canvas type registry.\n\nThis operation is potentially effectful: for a persisted cold chat, the host\nmay resume its backing runtime, discover extensions, and execute extension\nstartup code. It MUST NOT create a backing for a brand-new chat, send a user\nmessage, or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present because live provider\nregistration changes are published reactively.",
+ "description": "Requests materialization of the chat's Canvas type registry.\n\nA host MUST reject the action when the chat's agent does not advertise\n{@link AgentCapabilities.canvases} or when the chat cannot materialize the\nregistry. Materialization MUST NOT send a user message or invoke a model.\n\nThe reducer is a no-op. The host remains authoritative and publishes the\ncomplete result through\n{@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.\nClients do not need to dispatch this action when\n{@link ChatState.canvasRegistry} is already present.\n\nStability: 1.0 - Early development.",
"properties": {
"type": {
"const": "chat/canvasRegistryRequested"
@@ -10773,7 +10761,7 @@
},
"ChatCanvasRegistryChangedAction": {
"type": "object",
- "description": "The Canvas type registry for this exact chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.",
+ "description": "The Canvas type registry for this chat changed.\n\nReplaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.\nSet to `undefined` when the registry is no longer materialized or available.\n\nStability: 1.0 - Early development.",
"properties": {
"type": {
"const": "chat/canvasRegistryChanged"
From a9c3184fa977f5907216a91a27d853c3d1134aad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Wed, 4 Nov 2026 23:01:09 -0800
Subject: [PATCH 11/12] test: preserve resource-read and Canvas fixtures
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
clients/go/ahptypes/roundtrip_fixture_test.go | 2 ++
1 file changed, 2 insertions(+)
diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go
index d5b923f73..e6024e6c6 100644
--- a/clients/go/ahptypes/roundtrip_fixture_test.go
+++ b/clients/go/ahptypes/roundtrip_fixture_test.go
@@ -316,6 +316,8 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string {
return enc(&v)
case "ResourceReadResult":
var v ResourceReadResult
+ dec(&v)
+ return enc(&v)
case "OpenCanvasParams":
var v OpenCanvasParams
dec(&v)
From dfb157a5dc385964ecbc67072629b3f309bfea9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?=
<1588988+jruales@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:54:01 -0800
Subject: [PATCH 12/12] canvas: apply mechanical review feedback
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Generated/Commands.generated.cs | 8 +--
.../JsonSerializerContext.generated.cs | 4 +-
.../Generated/State.generated.cs | 39 +++++++--------
clients/go/ahptypes/commands.generated.go | 4 --
clients/go/ahptypes/state.generated.go | 37 ++++++--------
.../generated/Commands.generated.kt | 28 -----------
.../generated/State.generated.kt | 29 +++++------
clients/rust/crates/ahp-types/src/commands.rs | 4 --
clients/rust/crates/ahp-types/src/state.rs | 37 ++++++--------
.../Generated/Commands.generated.swift | 24 ---------
.../Generated/State.generated.swift | 49 ++++++++++---------
docs/specification/chat-channel.md | 6 +--
schema/actions.schema.json | 36 +++++++-------
schema/commands.schema.json | 40 +++++++--------
schema/errors.schema.json | 40 +++++++--------
schema/notifications.schema.json | 36 +++++++-------
schema/state.schema.json | 36 +++++++-------
scripts/generate-csharp.ts | 4 +-
scripts/generate-go.ts | 4 +-
scripts/generate-kotlin.ts | 2 +-
scripts/generate-rust.ts | 4 +-
scripts/generate-swift.ts | 2 +-
scripts/read-stability.test.ts | 2 +-
types/channels-canvas/commands.ts | 2 -
types/channels-canvas/state.ts | 37 ++++++--------
...t-canvasregistrychanged-sets-registry.json | 16 +++---
.../round-trips/086-chat-canvas-registry.json | 24 ++++-----
27 files changed, 234 insertions(+), 320 deletions(-)
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 6cf9e29e6..47e854a32 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -1158,9 +1158,7 @@ public sealed record MoveChatResult
/// authoritative membership, availability, and presentation state.
///
/// Clients MUST NOT call this command unless the chat's agent advertises
-/// {@link AgentCapabilities.canvases}.
-///
-/// Stability: 1.0 - Early development.
+/// {@link AgentCapabilities.canvases}.
public sealed record OpenCanvasParams
{
/// Client-chosen URI for the canvas resource, for example
@@ -1190,9 +1188,7 @@ public sealed record OpenCanvasParams
/// resource is gone is a no-op.
///
/// Clients MUST NOT call this command unless the chat's agent advertises
-/// {@link AgentCapabilities.canvases}.
-///
-/// Stability: 1.0 - Early development.
+/// {@link AgentCapabilities.canvases}.
public sealed record DisposeCanvasParams
{
/// URI of the canvas resource to dispose.
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index afb82dd22..85383305a 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -84,12 +84,12 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(BackgroundWork))]
[JsonSerializable(typeof(BackgroundWorkKind))]
[JsonSerializable(typeof(CanvasesCapability))]
-[JsonSerializable(typeof(CanvasExtensionDeclaration))]
+[JsonSerializable(typeof(CanvasExtension))]
[JsonSerializable(typeof(CanvasReference))]
[JsonSerializable(typeof(CanvasRegistry))]
[JsonSerializable(typeof(CanvasState))]
[JsonSerializable(typeof(CanvasStateChangedAction))]
-[JsonSerializable(typeof(CanvasTypeDeclaration))]
+[JsonSerializable(typeof(CanvasType))]
[JsonSerializable(typeof(Changeset))]
[JsonSerializable(typeof(ChangesetCapabilities))]
[JsonSerializable(typeof(ChangesetClearedAction))]
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 58a5aa7bb..c3e1d56ee 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -2643,35 +2643,29 @@ public sealed class ChatState
public Dictionary? Meta { get; set; }
}
-/// One namespace that declares canvas types for a chat.
-///
-/// `extensionId` is opaque. Clients MUST NOT infer management behavior by
-/// parsing it.
-public sealed record CanvasExtensionDeclaration
+/// One extension or provider that declares canvas types for a chat.
+public sealed record CanvasExtension
{
- /// Stable opaque declaration namespace.
- public required string ExtensionId { get; init; }
+ /// Stable opaque identifier.
+ public required string Id { get; init; }
- /// Human-readable namespace title, when available.
+ /// Human-readable label, when available.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public string? ExtensionName { get; init; }
+ public string? Label { get; init; }
- /// Canvas types declared by this extension/provider. `canvasId` MUST be
+ /// Canvas types declared by this extension/provider. `id` MUST be
/// unique within this array.
- public required List Canvases { get; init; }
+ public required List Canvases { get; init; }
}
-/// One canvas type currently available to open.
-///
-/// This experimental object may gain optional discovery metadata. Clients
-/// SHOULD ignore fields they do not understand.
-public sealed record CanvasTypeDeclaration
+/// One canvas type currently available to open.
+public sealed record CanvasType
{
/// Provider-local canvas type identifier.
- public required string CanvasId { get; init; }
+ public required string Id { get; init; }
- /// Human-readable canvas type title.
- public required string Title { get; init; }
+ /// Human-readable canvas type label.
+ public required string Label { get; init; }
/// Description of what the canvas type does.
public required string Description { get; init; }
@@ -2683,9 +2677,10 @@ public sealed record CanvasTypeDeclaration
/// Canvas types currently registered for one chat.
public sealed record CanvasRegistry
{
- /// Declaring namespaces. `extensionId` MUST be unique within this
- /// array. An empty array is an authoritative loaded registry with no types.
- public required List Extensions { get; init; }
+ /// Extensions and providers that declare canvas types. `id` MUST be unique
+ /// within this array. An empty array is an authoritative loaded registry with
+ /// no types.
+ public required List Extensions { get; init; }
}
/// Stable reference to a subscribable canvas channel.
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 49b42752e..dd2b55181 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -776,8 +776,6 @@ type MoveChatResult struct {
//
// Clients MUST NOT call this command unless the chat's agent advertises
// {@link AgentCapabilities.canvases}.
-//
-// Stability: 1.0 - Early development.
type OpenCanvasParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
@@ -800,8 +798,6 @@ type OpenCanvasParams struct {
//
// Clients MUST NOT call this command unless the chat's agent advertises
// {@link AgentCapabilities.canvases}.
-//
-// Stability: 1.0 - Early development.
type DisposeCanvasParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index 42d60ed79..17e97dd01 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -1512,29 +1512,23 @@ type ChatState struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
-// One namespace that declares canvas types for a chat.
-//
-// `extensionId` is opaque. Clients MUST NOT infer management behavior by
-// parsing it.
-type CanvasExtensionDeclaration struct {
- // Stable opaque declaration namespace.
- ExtensionId string `json:"extensionId"`
- // Human-readable namespace title, when available.
- ExtensionName *string `json:"extensionName,omitempty"`
- // Canvas types declared by this extension/provider. `canvasId` MUST be
+// One extension or provider that declares canvas types for a chat.
+type CanvasExtension struct {
+ // Stable opaque identifier.
+ Id string `json:"id"`
+ // Human-readable label, when available.
+ Label *string `json:"label,omitempty"`
+ // Canvas types declared by this extension/provider. `id` MUST be
// unique within this array.
- Canvases []CanvasTypeDeclaration `json:"canvases"`
+ Canvases []CanvasType `json:"canvases"`
}
// One canvas type currently available to open.
-//
-// This experimental object may gain optional discovery metadata. Clients
-// SHOULD ignore fields they do not understand.
-type CanvasTypeDeclaration struct {
+type CanvasType struct {
// Provider-local canvas type identifier.
- CanvasId string `json:"canvasId"`
- // Human-readable canvas type title.
- Title string `json:"title"`
+ Id string `json:"id"`
+ // Human-readable canvas type label.
+ Label string `json:"label"`
// Description of what the canvas type does.
Description string `json:"description"`
// Whether opening this canvas requires an input flow.
@@ -1543,9 +1537,10 @@ type CanvasTypeDeclaration struct {
// Canvas types currently registered for one chat.
type CanvasRegistry struct {
- // Declaring namespaces. `extensionId` MUST be unique within this
- // array. An empty array is an authoritative loaded registry with no types.
- Extensions []CanvasExtensionDeclaration `json:"extensions"`
+ // Extensions and providers that declare canvas types. `id` MUST be unique
+ // within this array. An empty array is an authoritative loaded registry with
+ // no types.
+ Extensions []CanvasExtension `json:"extensions"`
}
// Stable reference to a subscribable canvas channel.
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index a8012f50b..d65579687 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -1024,22 +1024,6 @@ data class MoveChatResult(
val session: String
)
-/**
- * Opens one client-addressed canvas resource in a chat.
- *
- * The resulting live instance is published through the chat's
- * {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
- * Repeating this command with the same `channel` is idempotent. Reusing it for
- * a different chat, extension, or canvas type MUST fail with `Conflict`.
- * Clients MUST observe
- * {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
- * authoritative membership, availability, and presentation state.
- *
- * Clients MUST NOT call this command unless the chat's agent advertises
- * {@link AgentCapabilities.canvases}.
- *
- * Stability: 1.0 - Early development.
- */
@Serializable
data class OpenCanvasParams(
/**
@@ -1066,18 +1050,6 @@ data class OpenCanvasParams(
val canvasId: String
)
-/**
- * Closes and disposes one live canvas resource.
- *
- * The server removes the resource from the owning chat's
- * {@link ChatState.canvases} references. Repeating this command after the
- * resource is gone is a no-op.
- *
- * Clients MUST NOT call this command unless the chat's agent advertises
- * {@link AgentCapabilities.canvases}.
- *
- * Stability: 1.0 - Early development.
- */
@Serializable
data class DisposeCanvasParams(
/**
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index c52477171..630de2367 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -2280,32 +2280,32 @@ data class BackgroundSubagentWork(
)
@Serializable
-data class CanvasExtensionDeclaration(
+data class CanvasExtension(
/**
- * Stable opaque declaration namespace.
+ * Stable opaque identifier.
*/
- val extensionId: String,
+ val id: String,
/**
- * Human-readable namespace title, when available.
+ * Human-readable label, when available.
*/
- val extensionName: String? = null,
+ val label: String? = null,
/**
- * Canvas types declared by this extension/provider. `canvasId` MUST be
+ * Canvas types declared by this extension/provider. `id` MUST be
* unique within this array.
*/
- val canvases: List
+ val canvases: List
)
@Serializable
-data class CanvasTypeDeclaration(
+data class CanvasType(
/**
* Provider-local canvas type identifier.
*/
- val canvasId: String,
+ val id: String,
/**
- * Human-readable canvas type title.
+ * Human-readable canvas type label.
*/
- val title: String,
+ val label: String,
/**
* Description of what the canvas type does.
*/
@@ -2319,10 +2319,11 @@ data class CanvasTypeDeclaration(
@Serializable
data class CanvasRegistry(
/**
- * Declaring namespaces. `extensionId` MUST be unique within this
- * array. An empty array is an authoritative loaded registry with no types.
+ * Extensions and providers that declare canvas types. `id` MUST be unique
+ * within this array. An empty array is an authoritative loaded registry with
+ * no types.
*/
- val extensions: List
+ val extensions: List
)
@Serializable
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index b701e926c..89768249a 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -1067,8 +1067,6 @@ pub struct MoveChatResult {
///
/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
-///
-/// Stability: 1.0 - Early development.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenCanvasParams {
@@ -1094,8 +1092,6 @@ pub struct OpenCanvasParams {
///
/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
-///
-/// Stability: 1.0 - Early development.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisposeCanvasParams {
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index 7e6a0bca2..0637c764d 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -2193,34 +2193,28 @@ pub struct ChatState {
pub meta: Option,
}
-/// One namespace that declares canvas types for a chat.
-///
-/// `extensionId` is opaque. Clients MUST NOT infer management behavior by
-/// parsing it.
+/// One extension or provider that declares canvas types for a chat.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct CanvasExtensionDeclaration {
- /// Stable opaque declaration namespace.
- pub extension_id: String,
- /// Human-readable namespace title, when available.
+pub struct CanvasExtension {
+ /// Stable opaque identifier.
+ pub id: String,
+ /// Human-readable label, when available.
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub extension_name: Option,
- /// Canvas types declared by this extension/provider. `canvasId` MUST be
+ pub label: Option,
+ /// Canvas types declared by this extension/provider. `id` MUST be
/// unique within this array.
- pub canvases: Vec,
+ pub canvases: Vec,
}
/// One canvas type currently available to open.
-///
-/// This experimental object may gain optional discovery metadata. Clients
-/// SHOULD ignore fields they do not understand.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct CanvasTypeDeclaration {
+pub struct CanvasType {
/// Provider-local canvas type identifier.
- pub canvas_id: String,
- /// Human-readable canvas type title.
- pub title: String,
+ pub id: String,
+ /// Human-readable canvas type label.
+ pub label: String,
/// Description of what the canvas type does.
pub description: String,
/// Whether opening this canvas requires an input flow.
@@ -2231,9 +2225,10 @@ pub struct CanvasTypeDeclaration {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanvasRegistry {
- /// Declaring namespaces. `extensionId` MUST be unique within this
- /// array. An empty array is an authoritative loaded registry with no types.
- pub extensions: Vec,
+ /// Extensions and providers that declare canvas types. `id` MUST be unique
+ /// within this array. An empty array is an authoritative loaded registry with
+ /// no types.
+ pub extensions: Vec,
}
/// Stable reference to a subscribable canvas channel.
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index b8adc0d7a..95125a7c3 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -1154,20 +1154,6 @@ public struct MoveChatResult: Codable, Sendable {
}
}
-/// Opens one client-addressed canvas resource in a chat.
-///
-/// The resulting live instance is published through the chat's
-/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
-/// Repeating this command with the same `channel` is idempotent. Reusing it for
-/// a different chat, extension, or canvas type MUST fail with `Conflict`.
-/// Clients MUST observe
-/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
-/// authoritative membership, availability, and presentation state.
-///
-/// Clients MUST NOT call this command unless the chat's agent advertises
-/// {@link AgentCapabilities.canvases}.
-///
-/// Stability: 1.0 - Early development.
public struct OpenCanvasParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
@@ -1204,16 +1190,6 @@ public struct OpenCanvasParams: Codable, Sendable {
}
}
-/// Closes and disposes one live canvas resource.
-///
-/// The server removes the resource from the owning chat's
-/// {@link ChatState.canvases} references. Repeating this command after the
-/// resource is gone is a no-op.
-///
-/// Clients MUST NOT call this command unless the chat's agent advertises
-/// {@link AgentCapabilities.canvases}.
-///
-/// Stability: 1.0 - Early development.
public struct DisposeCanvasParams: Codable, Sendable {
/// Channel URI this command targets.
public var channel: String
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index 9eb5add39..d469fc200 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -6239,56 +6239,57 @@ public struct EmptyState: Codable, Sendable {
}
}
-public struct CanvasExtensionDeclaration: Codable, Sendable {
- /// Stable opaque declaration namespace.
- public var extensionId: String
- /// Human-readable namespace title, when available.
- public var extensionName: String?
- /// Canvas types declared by this extension/provider. `canvasId` MUST be
+public struct CanvasExtension: Codable, Sendable {
+ /// Stable opaque identifier.
+ public var id: String
+ /// Human-readable label, when available.
+ public var label: String?
+ /// Canvas types declared by this extension/provider. `id` MUST be
/// unique within this array.
- public var canvases: [CanvasTypeDeclaration]
+ public var canvases: [CanvasType]
public init(
- extensionId: String,
- extensionName: String? = nil,
- canvases: [CanvasTypeDeclaration]
+ id: String,
+ label: String? = nil,
+ canvases: [CanvasType]
) {
- self.extensionId = extensionId
- self.extensionName = extensionName
+ self.id = id
+ self.label = label
self.canvases = canvases
}
}
-public struct CanvasTypeDeclaration: Codable, Sendable {
+public struct CanvasType: Codable, Sendable {
/// Provider-local canvas type identifier.
- public var canvasId: String
- /// Human-readable canvas type title.
- public var title: String
+ public var id: String
+ /// Human-readable canvas type label.
+ public var label: String
/// Description of what the canvas type does.
public var description: String
/// Whether opening this canvas requires an input flow.
public var requiresInput: Bool
public init(
- canvasId: String,
- title: String,
+ id: String,
+ label: String,
description: String,
requiresInput: Bool
) {
- self.canvasId = canvasId
- self.title = title
+ self.id = id
+ self.label = label
self.description = description
self.requiresInput = requiresInput
}
}
public struct CanvasRegistry: Codable, Sendable {
- /// Declaring namespaces. `extensionId` MUST be unique within this
- /// array. An empty array is an authoritative loaded registry with no types.
- public var extensions: [CanvasExtensionDeclaration]
+ /// Extensions and providers that declare canvas types. `id` MUST be unique
+ /// within this array. An empty array is an authoritative loaded registry with
+ /// no types.
+ public var extensions: [CanvasExtension]
public init(
- extensions: [CanvasExtensionDeclaration]
+ extensions: [CanvasExtension]
) {
self.extensions = extensions
}
diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md
index b304dd7da..39206862c 100644
--- a/docs/specification/chat-channel.md
+++ b/docs/specification/chat-channel.md
@@ -96,10 +96,8 @@ separate from [`canvases`](/reference/chat#chatstate), which contains references
to already-open live Canvas **instances**.
The registry normalizes extension/provider metadata: each
-[`CanvasExtensionDeclaration`](/reference/canvas#canvasextensiondeclaration)
-appears once and carries its own Canvas types. Clients treat `extensionId` as
-opaque, key a type by `(extensionId, canvasId)`, and do not infer source or
-management behavior from the identifier's text.
+[`CanvasExtension`](/reference/canvas#canvasextension) appears once and carries
+its own Canvas types. Clients key a type by its extension and Canvas type IDs.
For a live chat, hosts keep this registry synchronized with provider
registration and dispatch
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index 0335f81d5..fa4b83800 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -7518,42 +7518,42 @@
"title"
]
},
- "CanvasExtensionDeclaration": {
+ "CanvasExtension": {
"type": "object",
- "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
+ "description": "One extension or provider that declares canvas types for a chat.",
"properties": {
- "extensionId": {
+ "id": {
"type": "string",
- "description": "Stable opaque declaration namespace."
+ "description": "Stable opaque identifier."
},
- "extensionName": {
+ "label": {
"type": "string",
- "description": "Human-readable namespace title, when available."
+ "description": "Human-readable label, when available."
},
"canvases": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
+ "$ref": "#/$defs/CanvasType"
},
- "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ "description": "Canvas types declared by this extension/provider. `id` MUST be\nunique within this array."
}
},
"required": [
- "extensionId",
+ "id",
"canvases"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasType": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.",
"properties": {
- "canvasId": {
+ "id": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "title": {
+ "label": {
"type": "string",
- "description": "Human-readable canvas type title."
+ "description": "Human-readable canvas type label."
},
"description": {
"type": "string",
@@ -7565,8 +7565,8 @@
}
},
"required": [
- "canvasId",
- "title",
+ "id",
+ "label",
"description",
"requiresInput"
]
@@ -7578,9 +7578,9 @@
"extensions": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasExtensionDeclaration"
+ "$ref": "#/$defs/CanvasExtension"
},
- "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Extensions and providers that declare canvas types. `id` MUST be unique\nwithin this array. An empty array is an authoritative loaded registry with\nno types."
}
},
"required": [
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index 7950025d5..2b6e5634d 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -1578,7 +1578,7 @@
},
"OpenCanvasParams": {
"type": "object",
- "description": "Opens one client-addressed canvas resource in a chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` is idempotent. Reusing it for\na different chat, extension, or canvas type MUST fail with `Conflict`.\nClients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
+ "description": "Opens one client-addressed canvas resource in a chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` is idempotent. Reusing it for\na different chat, extension, or canvas type MUST fail with `Conflict`.\nClients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -1611,7 +1611,7 @@
},
"DisposeCanvasParams": {
"type": "object",
- "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references. Repeating this command after the\nresource is gone is a no-op.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
+ "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references. Repeating this command after the\nresource is gone is a no-op.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -6841,42 +6841,42 @@
"title"
]
},
- "CanvasExtensionDeclaration": {
+ "CanvasExtension": {
"type": "object",
- "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
+ "description": "One extension or provider that declares canvas types for a chat.",
"properties": {
- "extensionId": {
+ "id": {
"type": "string",
- "description": "Stable opaque declaration namespace."
+ "description": "Stable opaque identifier."
},
- "extensionName": {
+ "label": {
"type": "string",
- "description": "Human-readable namespace title, when available."
+ "description": "Human-readable label, when available."
},
"canvases": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
+ "$ref": "#/$defs/CanvasType"
},
- "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ "description": "Canvas types declared by this extension/provider. `id` MUST be\nunique within this array."
}
},
"required": [
- "extensionId",
+ "id",
"canvases"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasType": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.",
"properties": {
- "canvasId": {
+ "id": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "title": {
+ "label": {
"type": "string",
- "description": "Human-readable canvas type title."
+ "description": "Human-readable canvas type label."
},
"description": {
"type": "string",
@@ -6888,8 +6888,8 @@
}
},
"required": [
- "canvasId",
- "title",
+ "id",
+ "label",
"description",
"requiresInput"
]
@@ -6901,9 +6901,9 @@
"extensions": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasExtensionDeclaration"
+ "$ref": "#/$defs/CanvasExtension"
},
- "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Extensions and providers that declare canvas types. `id` MUST be unique\nwithin this array. An empty array is an authoritative loaded registry with\nno types."
}
},
"required": [
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 4a6678623..ebe8fed9e 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -4864,42 +4864,42 @@
"title"
]
},
- "CanvasExtensionDeclaration": {
+ "CanvasExtension": {
"type": "object",
- "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
+ "description": "One extension or provider that declares canvas types for a chat.",
"properties": {
- "extensionId": {
+ "id": {
"type": "string",
- "description": "Stable opaque declaration namespace."
+ "description": "Stable opaque identifier."
},
- "extensionName": {
+ "label": {
"type": "string",
- "description": "Human-readable namespace title, when available."
+ "description": "Human-readable label, when available."
},
"canvases": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
+ "$ref": "#/$defs/CanvasType"
},
- "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ "description": "Canvas types declared by this extension/provider. `id` MUST be\nunique within this array."
}
},
"required": [
- "extensionId",
+ "id",
"canvases"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasType": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.",
"properties": {
- "canvasId": {
+ "id": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "title": {
+ "label": {
"type": "string",
- "description": "Human-readable canvas type title."
+ "description": "Human-readable canvas type label."
},
"description": {
"type": "string",
@@ -4911,8 +4911,8 @@
}
},
"required": [
- "canvasId",
- "title",
+ "id",
+ "label",
"description",
"requiresInput"
]
@@ -4924,9 +4924,9 @@
"extensions": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasExtensionDeclaration"
+ "$ref": "#/$defs/CanvasExtension"
},
- "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Extensions and providers that declare canvas types. `id` MUST be unique\nwithin this array. An empty array is an authoritative loaded registry with\nno types."
}
},
"required": [
@@ -7800,7 +7800,7 @@
},
"OpenCanvasParams": {
"type": "object",
- "description": "Opens one client-addressed canvas resource in a chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` is idempotent. Reusing it for\na different chat, extension, or canvas type MUST fail with `Conflict`.\nClients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
+ "description": "Opens one client-addressed canvas resource in a chat.\n\nThe resulting live instance is published through the chat's\n{@link ChatState.canvases} references and its `ahp-canvas:` state channel.\nRepeating this command with the same `channel` is idempotent. Reusing it for\na different chat, extension, or canvas type MUST fail with `Conflict`.\nClients MUST observe\n{@link ChatState.canvases} and the referenced `ahp-canvas:` channel for\nauthoritative membership, availability, and presentation state.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -7833,7 +7833,7 @@
},
"DisposeCanvasParams": {
"type": "object",
- "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references. Repeating this command after the\nresource is gone is a no-op.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.\n\nStability: 1.0 - Early development.",
+ "description": "Closes and disposes one live canvas resource.\n\nThe server removes the resource from the owning chat's\n{@link ChatState.canvases} references. Repeating this command after the\nresource is gone is a no-op.\n\nClients MUST NOT call this command unless the chat's agent advertises\n{@link AgentCapabilities.canvases}.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index 96c43bcad..72178a34f 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -5166,42 +5166,42 @@
"title"
]
},
- "CanvasExtensionDeclaration": {
+ "CanvasExtension": {
"type": "object",
- "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
+ "description": "One extension or provider that declares canvas types for a chat.",
"properties": {
- "extensionId": {
+ "id": {
"type": "string",
- "description": "Stable opaque declaration namespace."
+ "description": "Stable opaque identifier."
},
- "extensionName": {
+ "label": {
"type": "string",
- "description": "Human-readable namespace title, when available."
+ "description": "Human-readable label, when available."
},
"canvases": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
+ "$ref": "#/$defs/CanvasType"
},
- "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ "description": "Canvas types declared by this extension/provider. `id` MUST be\nunique within this array."
}
},
"required": [
- "extensionId",
+ "id",
"canvases"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasType": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.",
"properties": {
- "canvasId": {
+ "id": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "title": {
+ "label": {
"type": "string",
- "description": "Human-readable canvas type title."
+ "description": "Human-readable canvas type label."
},
"description": {
"type": "string",
@@ -5213,8 +5213,8 @@
}
},
"required": [
- "canvasId",
- "title",
+ "id",
+ "label",
"description",
"requiresInput"
]
@@ -5226,9 +5226,9 @@
"extensions": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasExtensionDeclaration"
+ "$ref": "#/$defs/CanvasExtension"
},
- "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Extensions and providers that declare canvas types. `id` MUST be unique\nwithin this array. An empty array is an authoritative loaded registry with\nno types."
}
},
"required": [
diff --git a/schema/state.schema.json b/schema/state.schema.json
index 3e0b3b714..86bc9bced 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -4775,42 +4775,42 @@
"title"
]
},
- "CanvasExtensionDeclaration": {
+ "CanvasExtension": {
"type": "object",
- "description": "One namespace that declares canvas types for a chat.\n\n`extensionId` is opaque. Clients MUST NOT infer management behavior by\nparsing it.",
+ "description": "One extension or provider that declares canvas types for a chat.",
"properties": {
- "extensionId": {
+ "id": {
"type": "string",
- "description": "Stable opaque declaration namespace."
+ "description": "Stable opaque identifier."
},
- "extensionName": {
+ "label": {
"type": "string",
- "description": "Human-readable namespace title, when available."
+ "description": "Human-readable label, when available."
},
"canvases": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasTypeDeclaration"
+ "$ref": "#/$defs/CanvasType"
},
- "description": "Canvas types declared by this extension/provider. `canvasId` MUST be\nunique within this array."
+ "description": "Canvas types declared by this extension/provider. `id` MUST be\nunique within this array."
}
},
"required": [
- "extensionId",
+ "id",
"canvases"
]
},
- "CanvasTypeDeclaration": {
+ "CanvasType": {
"type": "object",
- "description": "One canvas type currently available to open.\n\nThis experimental object may gain optional discovery metadata. Clients\nSHOULD ignore fields they do not understand.",
+ "description": "One canvas type currently available to open.",
"properties": {
- "canvasId": {
+ "id": {
"type": "string",
"description": "Provider-local canvas type identifier."
},
- "title": {
+ "label": {
"type": "string",
- "description": "Human-readable canvas type title."
+ "description": "Human-readable canvas type label."
},
"description": {
"type": "string",
@@ -4822,8 +4822,8 @@
}
},
"required": [
- "canvasId",
- "title",
+ "id",
+ "label",
"description",
"requiresInput"
]
@@ -4835,9 +4835,9 @@
"extensions": {
"type": "array",
"items": {
- "$ref": "#/$defs/CanvasExtensionDeclaration"
+ "$ref": "#/$defs/CanvasExtension"
},
- "description": "Declaring namespaces. `extensionId` MUST be unique within this\narray. An empty array is an authoritative loaded registry with no types."
+ "description": "Extensions and providers that declare canvas types. `id` MUST be unique\nwithin this array. An empty array is an authoritative loaded registry with\nno types."
}
},
"required": [
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index b9bde4c86..e1cd826ca 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -794,8 +794,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin
{ name: 'BackgroundShellWork' },
{ name: 'BackgroundSubagentWork' },
{ name: 'ChatState', mutable: true },
- { name: 'CanvasExtensionDeclaration' },
- { name: 'CanvasTypeDeclaration' },
+ { name: 'CanvasExtension' },
+ { name: 'CanvasType' },
{ name: 'CanvasRegistry' },
{ name: 'CanvasReference' },
{ name: 'CanvasState', mutable: true },
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index 4fe9aa81b..a53f60014 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -767,8 +767,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin
{ name: 'SessionChatSummary' },
{ name: 'ChangesSummary' },
{ name: 'ChatState' },
- { name: 'CanvasExtensionDeclaration' },
- { name: 'CanvasTypeDeclaration' },
+ { name: 'CanvasExtension' },
+ { name: 'CanvasType' },
{ name: 'CanvasRegistry' },
{ name: 'CanvasReference' },
{ name: 'CanvasState' },
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index fd7e995d5..cc8663e5e 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -1014,7 +1014,7 @@ const STATE_STRUCTS = [
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'BackgroundShellWork', 'BackgroundSubagentWork',
- 'CanvasExtensionDeclaration', 'CanvasTypeDeclaration', 'CanvasRegistry', 'CanvasReference', 'CanvasState',
+ 'CanvasExtension', 'CanvasType', 'CanvasRegistry', 'CanvasReference', 'CanvasState',
'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest',
'SessionToolAuthenticationRequest',
'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'ChatConfigState', 'Turn', 'ActiveTurn', 'Message',
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index a3b50887b..9f9422ddb 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -889,8 +889,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str
{ name: 'ConfigSchema' },
{ name: 'PendingMessage' },
{ name: 'ChatState' },
- { name: 'CanvasExtensionDeclaration' },
- { name: 'CanvasTypeDeclaration' },
+ { name: 'CanvasExtension' },
+ { name: 'CanvasType' },
{ name: 'CanvasRegistry' },
{ name: 'CanvasReference' },
{ name: 'CanvasState' },
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index 680300a7e..d6c4fc0b2 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -752,7 +752,7 @@ const STATE_STRUCTS = [
'TerminalRunningLifecycleState', 'TerminalExitedLifecycleState', 'TerminalState',
'TerminalUnclassifiedPart', 'TerminalCommandPart',
'UsageInfo', 'ErrorInfo', 'Snapshot', 'EmptyState',
- 'CanvasExtensionDeclaration', 'CanvasTypeDeclaration', 'CanvasRegistry', 'CanvasReference', 'CanvasState',
+ 'CanvasExtension', 'CanvasType', 'CanvasRegistry', 'CanvasReference', 'CanvasState',
'Changeset', 'ChangesetCapabilities', 'ChangesetState', 'ChangesetFile', 'ChangesetOperation',
'AnnotationsSummary', 'AnnotationsState', 'AnnotationOrigin', 'Annotation', 'AnnotationEntry',
'TelemetryCapabilities',
diff --git a/scripts/read-stability.test.ts b/scripts/read-stability.test.ts
index 64e8c6ae3..1f09aefe4 100644
--- a/scripts/read-stability.test.ts
+++ b/scripts/read-stability.test.ts
@@ -120,7 +120,7 @@ test('native client docs preserve declaration and entrypoint stability', () => {
['clients/dotnet/src/AgentHostProtocol.Abstractions/Generated', 'Commands.generated.cs', 'Notifications.generated.cs'],
];
for (const [directory, commands, notifications] of clients) {
- for (const [file, expected] of [[commands, 16], [notifications, 6]] as const) {
+ for (const [file, expected] of [[commands, 14], [notifications, 6]] as const) {
const content = readFileSync(resolve(root, directory, file), 'utf8');
assert.equal(content.split(marker).length - 1, expected, `${directory}/${file}`);
}
diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts
index f6c95e25d..a542e89dd 100644
--- a/types/channels-canvas/commands.ts
+++ b/types/channels-canvas/commands.ts
@@ -32,7 +32,6 @@ import type { ChatState } from '../channels-chat/state.js';
* @direction Client → Server
* @messageType Request
* @version 1
- * @stability 1.0
*/
export interface OpenCanvasParams extends BaseParams {
/**
@@ -65,7 +64,6 @@ export interface OpenCanvasParams extends BaseParams {
* @direction Client → Server
* @messageType Request
* @version 1
- * @stability 1.0
*/
export interface DisposeCanvasParams extends BaseParams {
/** URI of the canvas resource to dispose. */
diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts
index 6a4bd0f57..d8eabc4fc 100644
--- a/types/channels-canvas/state.ts
+++ b/types/channels-canvas/state.ts
@@ -10,38 +10,32 @@
import type { URI } from '../common/state.js';
/**
- * One namespace that declares canvas types for a chat.
- *
- * `extensionId` is opaque. Clients MUST NOT infer management behavior by
- * parsing it.
+ * One extension or provider that declares canvas types for a chat.
*
* @category Canvas State
*/
-export interface CanvasExtensionDeclaration {
- /** Stable opaque declaration namespace. */
- extensionId: string;
- /** Human-readable namespace title, when available. */
- extensionName?: string;
+export interface CanvasExtension {
+ /** Stable opaque identifier. */
+ id: string;
+ /** Human-readable label, when available. */
+ label?: string;
/**
- * Canvas types declared by this extension/provider. `canvasId` MUST be
+ * Canvas types declared by this extension/provider. `id` MUST be
* unique within this array.
*/
- canvases: CanvasTypeDeclaration[];
+ canvases: CanvasType[];
}
/**
* One canvas type currently available to open.
*
- * This experimental object may gain optional discovery metadata. Clients
- * SHOULD ignore fields they do not understand.
- *
* @category Canvas State
*/
-export interface CanvasTypeDeclaration {
+export interface CanvasType {
/** Provider-local canvas type identifier. */
- canvasId: string;
- /** Human-readable canvas type title. */
- title: string;
+ id: string;
+ /** Human-readable canvas type label. */
+ label: string;
/** Description of what the canvas type does. */
description: string;
/** Whether opening this canvas requires an input flow. */
@@ -55,10 +49,11 @@ export interface CanvasTypeDeclaration {
*/
export interface CanvasRegistry {
/**
- * Declaring namespaces. `extensionId` MUST be unique within this
- * array. An empty array is an authoritative loaded registry with no types.
+ * Extensions and providers that declare canvas types. `id` MUST be unique
+ * within this array. An empty array is an authoritative loaded registry with
+ * no types.
*/
- extensions: CanvasExtensionDeclaration[];
+ extensions: CanvasExtension[];
}
/**
diff --git a/types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json b/types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json
index 8b95af49d..0c897a5c2 100644
--- a/types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json
+++ b/types/test-cases/reducers/328-chat-canvasregistrychanged-sets-registry.json
@@ -14,12 +14,12 @@
"canvasRegistry": {
"extensions": [
{
- "extensionId": "opaque-provider-id",
- "extensionName": "Project Canvases",
+ "id": "opaque-provider-id",
+ "label": "Project Canvases",
"canvases": [
{
- "canvasId": "diagram",
- "title": "Diagram",
+ "id": "diagram",
+ "label": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
}
@@ -38,12 +38,12 @@
"canvasRegistry": {
"extensions": [
{
- "extensionId": "opaque-provider-id",
- "extensionName": "Project Canvases",
+ "id": "opaque-provider-id",
+ "label": "Project Canvases",
"canvases": [
{
- "canvasId": "diagram",
- "title": "Diagram",
+ "id": "diagram",
+ "label": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
}
diff --git a/types/test-cases/round-trips/086-chat-canvas-registry.json b/types/test-cases/round-trips/086-chat-canvas-registry.json
index b4994940b..d84e687d2 100644
--- a/types/test-cases/round-trips/086-chat-canvas-registry.json
+++ b/types/test-cases/round-trips/086-chat-canvas-registry.json
@@ -14,18 +14,18 @@
"canvasRegistry": {
"extensions": [
{
- "extensionId": "opaque-provider-id",
- "extensionName": "Diagram Tools",
+ "id": "opaque-provider-id",
+ "label": "Diagram Tools",
"canvases": [
{
- "canvasId": "diagram",
- "title": "Diagram",
+ "id": "diagram",
+ "label": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
},
{
- "canvasId": "form",
- "title": "Form",
+ "id": "form",
+ "label": "Form",
"description": "Collect structured input.",
"requiresInput": true
}
@@ -48,18 +48,18 @@
"canvasRegistry": {
"extensions": [
{
- "extensionId": "opaque-provider-id",
- "extensionName": "Diagram Tools",
+ "id": "opaque-provider-id",
+ "label": "Diagram Tools",
"canvases": [
{
- "canvasId": "diagram",
- "title": "Diagram",
+ "id": "diagram",
+ "label": "Diagram",
"description": "Visualize a diagram.",
"requiresInput": false
},
{
- "canvasId": "form",
- "title": "Form",
+ "id": "form",
+ "label": "Form",
"description": "Collect structured input.",
"requiresInput": true
}