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 }