From b0d3c5295f071ab1d0ee9b57bda8d77641b793bd Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Wed, 1 Jul 2026 21:49:21 -0500 Subject: [PATCH 1/5] Add Canvas channel (spec + all clients) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the Canvas surface proposed in #297. A Canvas is a host-driven, interactive UI surface (editor, browser, terminal, document view, …) that a session can open and drive over AHP. This adds the full protocol surface plus matching support in all five client mirrors. Protocol surface (types/): - ClientCapabilities.canvas opt-in capability. - Session-state discovery surface: SessionState.canvases (declared canvas types) and SessionState.openCanvases (live instances), plus SessionActiveClient.canvasProviders published atomically like tools. New supporting types CanvasAvailability, CanvasProviderKind, CanvasProviderSource (named discriminated union), SessionCanvasDeclaration, ClientCanvasDeclaration, OpenCanvasRef, SessionCanvasAction. - Per-instance channel ahp-canvas:/ carrying CanvasState and the canvas/updated, canvas/closeRequested, canvas/message actions, with canvasReducer applying a sparse merge for canvas/updated. - Server -> client canvas provider request family (canvasOpen / canvasInvokeAction / canvasClose) mirrored in CommandMap for symmetry with the resource* family, and client -> server canvasReadResource for the in-band content path. - New CanvasProviderError (-32012) with CanvasProviderErrorData. - Protocol version 0.5.1 -> 0.6.0; canvas actions registered at 0.6.0. Generators/tooling: canvas sources wired into find-protocol-sources, generate-action-origin, and the Go/Rust/Swift/Kotlin generators; regenerated client types, JSON schemas, action-origin table, and reference docs. Clients: hand-ported canvasReducer and the session canvas arms to Rust, Go, Swift, Kotlin, and TypeScript. The exhaustive Rust/Swift state mirrors also route the ahp-canvas: channel; the prefix-routing Go/TS mirrors need no change. Fixtures: 8 shared reducer fixtures (232-239) exercising the session registry replacement, open-canvas catalogue replacement, canvas/updated full and partial merges, and the closeRequested / message / unknown-action no-ops. Validated across the TypeScript, Rust, Go, Swift, and Kotlin fixture runners. Docs: new specification/canvas-channel.md and guide/canvases.md, plus subscriptions.md, session-channel.md, and sidebar updates. CHANGELOGs updated for the spec and all five clients. Refs #297 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 18 + clients/go/CHANGELOG.md | 12 + clients/go/ahp/reducers.go | 38 ++ clients/go/ahp/reducers_fixture_test.go | 2 + clients/go/ahptypes/actions.generated.go | 109 ++++ clients/go/ahptypes/commands.generated.go | 130 +++++ clients/go/ahptypes/errors.generated.go | 10 + clients/go/ahptypes/state.generated.go | 263 ++++++++- clients/go/ahptypes/version.generated.go | 3 +- clients/go/release-metadata.json | 1 + clients/kotlin/CHANGELOG.md | 10 + clients/kotlin/release-metadata.json | 1 + .../microsoft/agenthostprotocol/Reducers.kt | 25 + .../generated/Actions.generated.kt | 80 ++- .../generated/Commands.generated.kt | 150 ++++- .../generated/Errors.generated.kt | 16 + .../generated/State.generated.kt | 311 +++++++++- .../generated/Version.generated.kt | 3 +- .../FixtureDrivenReducerTest.kt | 13 + clients/rust/CHANGELOG.md | 10 + clients/rust/crates/ahp-types/src/actions.rs | 108 +++- clients/rust/crates/ahp-types/src/commands.rs | 156 +++++ clients/rust/crates/ahp-types/src/errors.rs | 13 + clients/rust/crates/ahp-types/src/state.rs | 237 +++++++- clients/rust/crates/ahp-types/src/version.rs | 4 +- .../crates/ahp/src/multi_host_state_mirror.rs | 24 +- clients/rust/crates/ahp/src/reducers.rs | 58 +- .../ahp/tests/multi_host_state_mirror.rs | 2 + clients/rust/release-metadata.json | 1 + .../Generated/Actions.generated.swift | 103 ++++ .../Generated/Commands.generated.swift | 174 +++++- .../Generated/Errors.generated.swift | 19 + .../Generated/State.generated.swift | 301 +++++++++- .../Generated/Version.generated.swift | 3 +- .../Sources/AgentHostProtocol/Reducers.swift | 43 ++ .../AHPStateMirror.swift | 9 + .../MultiHostStateMirror.swift | 10 + .../FixtureDrivenReducerTests.swift | 4 + clients/swift/CHANGELOG.md | 10 + clients/swift/release-metadata.json | 1 + clients/typescript/CHANGELOG.md | 12 + clients/typescript/release-metadata.json | 1 + docs/.vitepress/config.mts | 2 + docs/guide/canvases.md | 51 ++ docs/specification/canvas-channel.md | 208 +++++++ docs/specification/session-channel.md | 9 + docs/specification/subscriptions.md | 4 +- schema/actions.schema.json | 381 ++++++++++++ schema/commands.schema.json | 544 ++++++++++++++++++ schema/errors.schema.json | 472 ++++++++++++++- schema/notifications.schema.json | 258 +++++++++ schema/state.schema.json | 270 +++++++++ scripts/find-protocol-sources.ts | 1 + scripts/generate-action-origin.ts | 46 +- scripts/generate-go.ts | 60 +- scripts/generate-kotlin.ts | 42 +- scripts/generate-rust.ts | 55 +- scripts/generate-swift.ts | 40 +- types/action-origin.generated.ts | 32 ++ types/actions.ts | 1 + types/channels-canvas/actions.ts | 73 +++ types/channels-canvas/commands.ts | 221 +++++++ types/channels-canvas/reducer.ts | 45 ++ types/channels-canvas/state.ts | 65 +++ types/channels-session/actions.ts | 37 ++ types/channels-session/reducer.ts | 6 + types/channels-session/state.ts | 198 ++++++- types/commands.ts | 1 + types/common/actions.ts | 20 +- types/common/commands.ts | 12 + types/common/errors.ts | 28 + types/common/messages.ts | 23 + types/common/state.ts | 3 +- types/index.ts | 28 + types/messages.test.ts | 1 + types/reducers.test.ts | 6 +- types/reducers.ts | 1 + types/state.ts | 1 + ...session-canvaseschanged-sets-registry.json | 65 +++ ...on-opencanvaseschanged-sets-catalogue.json | 61 ++ .../234-canvas-updated-merges-all-fields.json | 37 ++ ...nvas-updated-partial-preserves-absent.json | 31 + ...-canvas-updated-partial-complementary.json | 31 + .../237-canvas-closerequested-no-op.json | 25 + .../reducers/238-canvas-message-no-op.json | 26 + ...9-canvas-unknown-action-type-is-no-op.json | 23 + types/version/message-checks.ts | 11 +- types/version/registry.ts | 8 +- 88 files changed, 6008 insertions(+), 53 deletions(-) create mode 100644 docs/guide/canvases.md create mode 100644 docs/specification/canvas-channel.md create mode 100644 types/channels-canvas/actions.ts create mode 100644 types/channels-canvas/commands.ts create mode 100644 types/channels-canvas/reducer.ts create mode 100644 types/channels-canvas/state.ts create mode 100644 types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json create mode 100644 types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json create mode 100644 types/test-cases/reducers/234-canvas-updated-merges-all-fields.json create mode 100644 types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json create mode 100644 types/test-cases/reducers/236-canvas-updated-partial-complementary.json create mode 100644 types/test-cases/reducers/237-canvas-closerequested-no-op.json create mode 100644 types/test-cases/reducers/238-canvas-message-no-op.json create mode 100644 types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee62817..8ec33872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ changes accumulate. Track in-flight protocol changes via PRs touching `NOTIFICATION_INTRODUCED_IN` maps in [`types/version/registry.ts`](types/version/registry.ts). +## [0.6.0] — Unreleased + +Spec version: `0.6.0` + ### Added - Optional `intention` field on `chat/toolCallStart` and every `ToolCallState` @@ -39,6 +43,20 @@ changes accumulate. Track in-flight protocol changes via PRs touching extends `PaginatedResult`, letting clients fetch a large session catalogue incrementally. Fully additive — omitting the fields preserves today's behaviour. +- Canvas channel — an opt-in surface for the agent to open rich, interactive UI + surfaces (document/spreadsheet editors, diff views, live previews) alongside a + session, following the "one channel per resource" model of terminals and + changesets. Adds the `ClientCapabilities.canvas` capability; the + `SessionState.canvases` registry and `SessionState.openCanvases` catalogue + (`session/canvasesChanged` / `session/openCanvasesChanged`) plus + `SessionActiveClient.canvasProviders` and the `SessionCanvasDeclaration` / + `ClientCanvasDeclaration` / `OpenCanvasRef` / `CanvasProviderSource` discovery + types; the per-instance `ahp-canvas:/` channel and its `CanvasState`; the + `canvas/updated`, `canvas/closeRequested`, and `canvas/message` actions; the + `canvasOpen` / `canvasInvokeAction` / `canvasClose` provider request family and + the client → server `canvasReadResource` content-fetch request; and the + `CanvasProviderError` (`-32012`) error. See + [`docs/specification/canvas-channel.md`](docs/specification/canvas-channel.md). ### Removed diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index fd43e073..29eefcaf 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -40,6 +40,18 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. lifecycle state. - Optional `Model` and `Tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +- Canvas channel support: the per-instance `CanvasState` plus the + `CanvasUpdatedAction` (wire `canvas/updated`), `CanvasCloseRequestedAction` + (`canvas/closeRequested`), and `CanvasMessageAction` (`canvas/message`) + actions, the `SessionCanvasesChangedAction` (`session/canvasesChanged`) and + `SessionOpenCanvasesChangedAction` (`session/openCanvasesChanged`) session + actions, and the canvas discovery types (`SessionCanvasDeclaration`, + `ClientCanvasDeclaration`, `OpenCanvasRef`, `CanvasProviderSource`) on + `SessionState.Canvases` / `SessionState.OpenCanvases`. Adds the + `ClientCapabilities.Canvas` capability, the `canvasOpen` / `canvasInvokeAction` + / `canvasClose` / `canvasReadResource` methods, and the `CanvasProviderError` + error. The session reducer replaces the canvas registry/catalogue and + `ApplyActionToCanvas` sparse-merges `canvas/updated`. ### Removed diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 45b4fc92..71666f49 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -738,6 +738,12 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct case *ahptypes.SessionServerToolsChangedAction: state.ServerTools = append([]ahptypes.ToolDefinition(nil), a.Tools...) return ReduceOutcomeApplied + case *ahptypes.SessionCanvasesChangedAction: + state.Canvases = append([]ahptypes.SessionCanvasDeclaration(nil), a.Canvases...) + return ReduceOutcomeApplied + case *ahptypes.SessionOpenCanvasesChangedAction: + state.OpenCanvases = append([]ahptypes.OpenCanvasRef(nil), a.OpenCanvases...) + return ReduceOutcomeApplied case *ahptypes.SessionActiveClientSetAction: for i := range state.ActiveClients { if state.ActiveClients[i].ClientId == a.ActiveClient.ClientId { @@ -1476,3 +1482,35 @@ func ApplyActionToResourceWatch(state *ahptypes.ResourceWatchState, action ahpty } return ReduceOutcomeOutOfScope } + +// ApplyActionToCanvas applies action to the [ahptypes.CanvasState] in +// place. `canvas/updated` is a sparse merge — a presented field +// (title, status, url, availability) overwrites the corresponding +// state field and an absent field preserves the current value. +// `canvas/closeRequested` and `canvas/message` are side-effect-only +// client→host signals the host acts on out of band, so they leave the +// state unchanged. Returns [ReduceOutcomeOutOfScope] for actions that +// target a different state tree. +func ApplyActionToCanvas(state *ahptypes.CanvasState, action ahptypes.StateAction) ReduceOutcome { + switch a := action.Value.(type) { + case *ahptypes.CanvasUpdatedAction: + if a.Title != nil { + state.Title = a.Title + } + if a.Status != nil { + state.Status = a.Status + } + if a.Url != nil { + state.Url = a.Url + } + if a.Availability != nil { + state.Availability = *a.Availability + } + return ReduceOutcomeApplied + case *ahptypes.CanvasCloseRequestedAction: + return ReduceOutcomeNoOp + case *ahptypes.CanvasMessageAction: + return ReduceOutcomeNoOp + } + return ReduceOutcomeOutOfScope +} diff --git a/clients/go/ahp/reducers_fixture_test.go b/clients/go/ahp/reducers_fixture_test.go index 9333353c..edaab2e2 100644 --- a/clients/go/ahp/reducers_fixture_test.go +++ b/clients/go/ahp/reducers_fixture_test.go @@ -160,6 +160,8 @@ func TestFixtureDrivenReducerParity(t *testing.T) { runFixture[ahptypes.AnnotationsState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToAnnotations) case "resourceWatch": runFixture[ahptypes.ResourceWatchState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToResourceWatch) + case "canvas": + runFixture[ahptypes.CanvasState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToCanvas) default: tt.Fatalf("unknown reducer kind %q", fixture.Reducer) } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index fd7b7d08..1b56fbb6 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -94,6 +94,11 @@ const ( ActionTypeTerminalCommandExecuted ActionType = "terminal/commandExecuted" ActionTypeTerminalCommandFinished ActionType = "terminal/commandFinished" ActionTypeResourceWatchChanged ActionType = "resourceWatch/changed" + ActionTypeSessionCanvasesChanged ActionType = "session/canvasesChanged" + ActionTypeSessionOpenCanvasesChanged ActionType = "session/openCanvasesChanged" + ActionTypeCanvasUpdated ActionType = "canvas/updated" + ActionTypeCanvasCloseRequested ActionType = "canvas/closeRequested" + ActionTypeCanvasMessage ActionType = "canvas/message" ) // ─── Action Envelope ───────────────────────────────────────────────── @@ -728,6 +733,29 @@ type SessionServerToolsChangedAction struct { Tools []ToolDefinition `json:"tools"` } +// The aggregated canvas registry for this session changed. +// +// Full-replacement semantics: the `canvases` array replaces +// {@link SessionState.canvases} entirely, mirroring +// `session/serverToolsChanged`. The host republishes the union of every +// connected provider (server-side and client-declared) whenever it changes. +type SessionCanvasesChangedAction struct { + Type ActionType `json:"type"` + // Updated canvas registry (full replacement). + Canvases []SessionCanvasDeclaration `json:"canvases"` +} + +// The catalogue of open canvas instances for this session changed. +// +// Full-replacement semantics: the `openCanvases` array replaces +// {@link SessionState.openCanvases} entirely. The host republishes the +// catalogue as instances open and close. +type SessionOpenCanvasesChangedAction struct { + Type ActionType `json:"type"` + // Updated open-instance catalogue (full replacement). + OpenCanvases []OpenCanvasRef `json:"openCanvases"` +} + // An active client for this session was added or updated. // // Upsert semantics keyed by {@link SessionActiveClient.clientId | `clientId`}: @@ -1233,6 +1261,52 @@ type ResourceWatchChangedAction struct { Changes json.RawMessage `json:"changes"` } +// The canvas instance's presentation state changed. +// +// Sparse-merge semantics: each present field overwrites the corresponding +// {@link CanvasState} field, and an absent field preserves the current value. +// There is no clear-to-absent via this action — that three-state distinction +// cannot survive JSON transport uniformly across languages, so a provider that +// needs to reset a field re-publishes it, and a full reset arrives as a fresh +// {@link CanvasState} snapshot on (re)subscribe. +type CanvasUpdatedAction struct { + Type ActionType `json:"type"` + // New title. Absent preserves the current title. + Title *string `json:"title,omitempty"` + // New provider-defined status. Absent preserves the current status. + Status *string `json:"status,omitempty"` + // New content address. Absent preserves the current url. + Url *string `json:"url,omitempty"` + // New availability. Absent preserves the current availability. + Availability *CanvasAvailability `json:"availability,omitempty"` +} + +// The user asked to close this canvas (e.g. hit the ✕ on the surface). +// +// A pure client→host signal with a no-op reducer, mirroring how +// `terminal/input` is a side-effect-only client action. The host runs the +// close flow in response — resolving `canvasClose` against the provider and +// dropping the instance from {@link SessionState.openCanvases} — rather than +// the reducer mutating channel state. +type CanvasCloseRequestedAction struct { + Type ActionType `json:"type"` +} + +// An opaque message relayed between the rendered canvas View and the +// instance's provider — the relay-carried analogue of a `postMessage` bridge. +// +// Bidirectional: a client dispatches it to carry a View→provider message, and +// the host emits it to carry a provider→View message (routed to the provider +// resolved for the instance, or handled host-internally for a server-side +// provider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it +// is a pure signal with a no-op reducer, so it never bloats channel state. See +// {@link /specification/canvas-channel | Canvas Channel}. +type CanvasMessageAction struct { + Type ActionType `json:"type"` + // Opaque, provider-defined message payload. + Payload json.RawMessage `json:"payload"` +} + // ─── StateAction Union ─────────────────────────────────────────────── // StateAction is the discriminated union of every state action. @@ -1283,6 +1357,8 @@ func (*SessionIsArchivedChangedAction) isStateAction() {} func (*SessionActivityChangedAction) isStateAction() {} func (*SessionChangesetsChangedAction) isStateAction() {} func (*SessionServerToolsChangedAction) isStateAction() {} +func (*SessionCanvasesChangedAction) isStateAction() {} +func (*SessionOpenCanvasesChangedAction) isStateAction() {} func (*SessionActiveClientSetAction) isStateAction() {} func (*SessionActiveClientRemovedAction) isStateAction() {} func (*SessionInputNeededSetAction) isStateAction() {} @@ -1319,6 +1395,9 @@ func (*TerminalCommandDetectionAvailableAction) isStateAction() {} func (*TerminalCommandExecutedAction) isStateAction() {} func (*TerminalCommandFinishedAction) isStateAction() {} func (*ResourceWatchChangedAction) isStateAction() {} +func (*CanvasUpdatedAction) isStateAction() {} +func (*CanvasCloseRequestedAction) isStateAction() {} +func (*CanvasMessageAction) isStateAction() {} // StateActionUnknown carries an unrecognized StateAction variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. type StateActionUnknown struct { @@ -1568,6 +1647,18 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "session/canvasesChanged": + var value SessionCanvasesChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/openCanvasesChanged": + var value SessionOpenCanvasesChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "session/activeClientSet": var value SessionActiveClientSetAction if err := json.Unmarshal(data, &value); err != nil { @@ -1784,6 +1875,24 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "canvas/updated": + var value CanvasUpdatedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "canvas/closeRequested": + var value CanvasCloseRequestedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "canvas/message": + var value CanvasMessageAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value default: raw := make(json.RawMessage, len(data)) copy(raw, data) diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index c7b03f49..c9169c74 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -157,6 +157,16 @@ type ClientCapabilities struct { // capability is declared. Clients that omit it MUST treat // App-bearing tool calls as ordinary MCP tool calls. McpApps map[string]json.RawMessage `json:"mcpApps,omitempty"` + // Client can render canvases and host client-declared canvas providers — it + // can render an opaque canvas URL in an isolated surface, and it can answer + // `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + // it declares via {@link SessionActiveClient.canvasProviders}. + // + // Hosts SHOULD only populate {@link SessionState.canvases} / + // {@link SessionState.openCanvases} and only route canvas requests to a + // client that declared this capability. Clients that omit it see no canvas + // surface. See {@link /specification/canvas-channel | Canvas Channel}. + Canvas map[string]json.RawMessage `json:"canvas,omitempty"` } // Re-establishes a dropped connection. The server replays missed actions or @@ -946,6 +956,126 @@ type ChangesetOperationFollowUp struct { External *bool `json:"external,omitempty"` } +// Opens a canvas instance against its provider. +// +// Sent by the host to the client that declared the target canvas via +// {@link SessionActiveClient.canvasProviders} (a client that also declared +// {@link ClientCapabilities.canvas}). For a server-side provider the host +// resolves the open host-internally and emits no request. The provider returns +// the initial render target and presentation fields, which the host folds into +// the new instance's {@link CanvasState}. +// +// Mirrors the `resource*` precedent: registered in `ServerCommandMap` and +// mirrored in `CommandMap` for symmetry. A client normally never initiates it — +// the host is not a canvas provider — and a receiver SHOULD reject a request +// whose target is not one of its declared providers. +type CanvasOpenParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Provider-local canvas id to open. + CanvasId string `json:"canvasId"` + // Owning provider id. + ExtensionId string `json:"extensionId"` + // Caller-minted handle for the new instance. + InstanceId string `json:"instanceId"` + // Open input, validated by the provider against its declared schema. + Input map[string]json.RawMessage `json:"input,omitempty"` +} + +// Result of the `canvasOpen` command. +type CanvasOpenResult struct { + // Initial content address for the instance (see {@link CanvasState.url}). + Url *string `json:"url,omitempty"` + // Initial title. + Title *string `json:"title,omitempty"` + // Initial provider-defined status. + Status *string `json:"status,omitempty"` +} + +// Invokes one of a canvas's declared actions against its provider. +// +// Sent by the host to the providing client (or resolved host-internally for a +// server-side provider) when the agent invokes a +// {@link SessionCanvasAction | declared action} on an open instance. The +// provider returns an opaque, provider-defined value. Registered symmetrically +// with the rest of the provider family (see {@link CanvasOpenParams}). +type CanvasInvokeActionParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Instance handle the action targets. + InstanceId string `json:"instanceId"` + // Provider-local canvas id of the instance. + CanvasId string `json:"canvasId"` + // Owning provider id. + ExtensionId string `json:"extensionId"` + // Declared action name to invoke. + ActionName string `json:"actionName"` + // Action input, validated by the provider against its declared schema. + Input map[string]json.RawMessage `json:"input,omitempty"` +} + +// Result of the `canvasInvokeAction` command. +type CanvasInvokeActionResult struct { + // Opaque, provider-defined return value. + Value *json.RawMessage `json:"value,omitempty"` +} + +// Closes a canvas instance against its provider. +// +// Sent by the host to the providing client (or resolved host-internally for a +// server-side provider) as part of the close flow — typically after a client +// dispatches `canvas/closeRequested`. The host then drops the instance from +// {@link SessionState.openCanvases}. Registered symmetrically with the rest of +// the provider family (see {@link CanvasOpenParams}). +type CanvasCloseParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Instance handle to close. + InstanceId string `json:"instanceId"` + // Provider-local canvas id of the instance. + CanvasId string `json:"canvasId"` + // Owning provider id. + ExtensionId string `json:"extensionId"` +} + +// Reads channel-served canvas content by `ahp-canvas-content:` URI. +// +// A client → host request, modeled on MCP's `resources/read`. When a canvas's +// {@link CanvasState.url} (or a sub-resource the rendered document references) +// is an `ahp-canvas-content://` address, the renderer cannot +// dial the host directly — for example in a relayed deployment behind a broker +// — so it resolves the bytes over the instance's existing `ahp-canvas:/` +// channel with this request instead of loading a network URL. The `` +// segment of the URI identifies which canvas channel to read from. See +// {@link /specification/canvas-channel | Canvas Channel}. +type CanvasReadResourceParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // An `ahp-canvas-content://` content URI to read. + Uri string `json:"uri"` +} + +// Result of the `canvasReadResource` command. +type CanvasReadResourceResult struct { + // The resolved content parts, wrapped for forward compatibility. + Contents []CanvasResourceContent `json:"contents"` +} + +// One resolved piece of channel-served canvas content. +// +// Carries exactly one of {@link text} (text payloads) or {@link blob} +// (base64-encoded binary payloads). +type CanvasResourceContent struct { + // The content URI this part resolves. + Uri string `json:"uri"` + // MIME type of the content, when known. + MimeType *string `json:"mimeType,omitempty"` + // UTF-8 text content, for text payloads. + Text *string `json:"text,omitempty"` + // Base64-encoded content, for binary payloads. + Blob *string `json:"blob,omitempty"` +} + // ─── ReconnectResult Union ──────────────────────────────────────────── // ReconnectResult is the result of the `reconnect` command. diff --git a/clients/go/ahptypes/errors.generated.go b/clients/go/ahptypes/errors.generated.go index 3b3e8f82..008c52d6 100644 --- a/clients/go/ahptypes/errors.generated.go +++ b/clients/go/ahptypes/errors.generated.go @@ -36,6 +36,8 @@ const ( ErrorCodeNotFound int32 = -32008 ErrorCodePermissionDenied int32 = -32009 ErrorCodeAlreadyExists int32 = -32010 + ErrorCodeConflict int32 = -32011 + ErrorCodeCanvasProviderError int32 = -32012 ) // AhpErrorCode is the type alias used by AHP application error codes. @@ -63,3 +65,11 @@ type PermissionDeniedErrorData struct { type UnsupportedProtocolVersionErrorData struct { SupportedVersions []string `json:"supportedVersions"` } + +// CanvasProviderErrorData is the detail payload of a +// CanvasProviderError (-32012) error. The Code is a provider-defined +// string (opaque to AHP) identifying the failure. +type CanvasProviderErrorData struct { + Code string `json:"code"` + Message string `json:"message"` +} diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 700beae5..898d0753 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -416,6 +416,28 @@ const ( ResourceChangeTypeDeleted ResourceChangeType = "deleted" ) +// Availability of a canvas provider or open instance. +type CanvasAvailability string + +const ( + // The provider is connected and can service requests for this canvas. + CanvasAvailabilityReady CanvasAvailability = "ready" + // The provider is temporarily unavailable (for example, a client provider + // that disconnected). The entry is retained so it can be restored when the + // provider reconnects; in-flight requests fail until then. + CanvasAvailabilityStale CanvasAvailability = "stale" +) + +// Discriminant for {@link CanvasProviderSource}. +type CanvasProviderKind string + +const ( + // The canvas is provided by the host itself. + CanvasProviderKindServer CanvasProviderKind = "server" + // The canvas is provided by a connected client. + CanvasProviderKindClient CanvasProviderKind = "client" +) + // ─── Structs ────────────────────────────────────────────────────────── // An optionally-sized icon that can be displayed in a user interface. @@ -767,6 +789,25 @@ type SessionState struct { // chats raise requests and removes them with `session/inputNeededRemoved` // once the underlying request resolves. InputNeeded []SessionInputRequest `json:"inputNeeded,omitempty"` + // Aggregated canvas registry currently exposed to the agent — the union of + // every connected provider (server-side and client-declared). Each entry + // describes a canvas the agent can open; the host folds + // {@link SessionActiveClient.canvasProviders | client-declared providers} + // into this list alongside its own server-side providers. + // + // Full-replacement via `session/canvasesChanged`. Populated only when at + // least one connected client declared {@link ClientCapabilities.canvas}; + // absent for sessions with no canvas surface. See + // {@link /specification/canvas-channel | Canvas Channel}. + Canvases []SessionCanvasDeclaration `json:"canvases,omitempty"` + // Lightweight catalogue of currently-open canvas instances. Each entry + // carries the instance's `ahp-canvas:/` channel URI so a subscriber can + // subscribe to the full {@link CanvasState} and render it — analogous to how + // {@link RootState.terminals} catalogues live terminals whose full state + // lives on each terminal channel. + // + // Full-replacement via `session/openCanvasesChanged`. + OpenCanvases []OpenCanvasRef `json:"openCanvases,omitempty"` // Additional provider-specific metadata for this session. // // Clients MAY look for well-known keys here to provide enhanced UI. @@ -794,6 +835,17 @@ type SessionActiveClient struct { // plugins in memory and rely on the host to expand them into concrete // children inside {@link SessionState.customizations}. Customizations []ClientPluginCustomization `json:"customizations,omitempty"` + // Canvas declarations this client contributes as a provider. Published + // atomically with the rest of the active-client entry via + // `session/activeClientSet` — exactly like {@link tools} — so there is no + // separate canvas-provider change action. The host folds these into + // {@link SessionState.canvases} with + // {@link SessionCanvasDeclaration.source | `source`} set to + // `{ kind: 'client', clientId }` and routes + // `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back + // to this client. Only meaningful for a client that declared + // {@link ClientCapabilities.canvas}. + CanvasProviders []ClientCanvasDeclaration `json:"canvasProviders,omitempty"` } // A user-input elicitation surfaced at the session level, mirroring one entry @@ -2966,6 +3018,140 @@ type ResourceChange struct { Type ResourceChangeType `json:"type"` } +// One named action a canvas exposes to the agent, mirroring the shape of a +// {@link ToolDefinition} entry. Unique within its owning +// `(extensionId, canvasId)`. +type SessionCanvasAction struct { + // Action name, unique within the owning `(extensionId, canvasId)`. + Name string `json:"name"` + // Human-readable description of what the action does. + Description *string `json:"description,omitempty"` + // JSON Schema for the action's input. Opaque to AHP; mirrors the + // {@link ToolDefinition.inputSchema} shape. + InputSchema map[string]json.RawMessage `json:"inputSchema,omitempty"` +} + +// One entry in the aggregated {@link SessionState.canvases} registry — a canvas +// the agent can open, contributed by a server-side or client-declared provider. +type SessionCanvasDeclaration struct { + // Owning provider id. Stable across declarations and instances. + ExtensionId string `json:"extensionId"` + // Human-readable provider name. + ExtensionName *string `json:"extensionName,omitempty"` + // Provider-local canvas id. Unique within `extensionId`. + CanvasId string `json:"canvasId"` + // Human-readable canvas name. + DisplayName string `json:"displayName"` + // Human-readable description of the canvas. + Description string `json:"description"` + // JSON Schema for the canvas's open input. Opaque to AHP; mirrors the + // {@link ToolDefinition.inputSchema} shape. + InputSchema map[string]json.RawMessage `json:"inputSchema,omitempty"` + // Actions this canvas exposes to the agent. + Actions []SessionCanvasAction `json:"actions,omitempty"` + // Where the declaration came from — for routing and cleanup. + Source CanvasProviderSource `json:"source"` +} + +// The lighter declaration shape a client publishes on +// {@link SessionActiveClient.canvasProviders}. The host derives the +// `extensionId` and {@link SessionCanvasDeclaration.source | `source`} when +// folding it into {@link SessionState.canvases}. +type ClientCanvasDeclaration struct { + // Provider-local canvas id, unique within the publishing client. + CanvasId string `json:"canvasId"` + // Human-readable canvas name. + DisplayName string `json:"displayName"` + // Human-readable description of the canvas. + Description string `json:"description"` + // JSON Schema for the canvas's open input. Opaque to AHP. + InputSchema map[string]json.RawMessage `json:"inputSchema,omitempty"` + // Actions this canvas exposes to the agent. + Actions []SessionCanvasAction `json:"actions,omitempty"` +} + +// A lightweight catalogue entry for one open canvas instance, surfaced on +// {@link SessionState.openCanvases}. The authoritative, mutable per-instance +// state lives on the instance's own {@link CanvasState} channel; this entry +// exists so a subscriber can discover the channel URI and render it without +// subscribing to every instance. +type OpenCanvasRef struct { + // Server-assigned instance handle, unique within the session. + InstanceId string `json:"instanceId"` + // The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load + // the full {@link CanvasState}. + Channel URI `json:"channel"` + // Provider-local canvas id this instance was opened from. + CanvasId string `json:"canvasId"` + // Owning provider id. + ExtensionId string `json:"extensionId"` + // Human-readable provider name. + ExtensionName *string `json:"extensionName,omitempty"` + // Current instance title, mirrored from {@link CanvasState.title}. + Title *string `json:"title,omitempty"` + // Whether the instance's provider is currently available. + Availability CanvasAvailability `json:"availability"` +} + +// A canvas provided by the host. Carries no `clientId` — server-side provider +// requests are resolved host-internally rather than routed to a peer. +type CanvasServerProviderSource struct { + Kind CanvasProviderKind `json:"kind"` +} + +// A canvas provided by a connected client. The host routes +// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas +// to the identified client. +type CanvasClientProviderSource struct { + Kind CanvasProviderKind `json:"kind"` + // `clientId` of the providing client (matches `initialize`). + ClientId string `json:"clientId"` +} + +// Full state for a single open canvas instance, delivered when a client +// subscribes to the instance's `ahp-canvas:/` channel. +// +// One channel exists per open instance — the same "one channel per resource" +// convention used by terminals, changesets, and resource watches. The +// lightweight catalogue entry that advertises this channel is +// {@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the +// authoritative, mutable per-instance view a renderer reads. +// +// Rendering is state-driven: a client renders the canvas by reading +// {@link url} and resolving it per the renderer's URL policy — directly for a +// reachable address, or over this channel via `canvasReadResource` for an +// `ahp-canvas-content:` address. It never receives a "render this" request. +type CanvasState struct { + // Server-assigned instance handle, unique within the session. + InstanceId string `json:"instanceId"` + // Provider-local canvas id this instance was opened from. + CanvasId string `json:"canvasId"` + // Owning provider id. + ExtensionId string `json:"extensionId"` + // Human-readable provider name. + ExtensionName *string `json:"extensionName,omitempty"` + // Human-readable canvas name. + DisplayName *string `json:"displayName,omitempty"` + // Input the agent supplied when opening the instance. Retained so the + // instance can be resumed or rebound after a reconnect. + Input map[string]json.RawMessage `json:"input,omitempty"` + // Current instance title. + Title *string `json:"title,omitempty"` + // Provider-defined status string (opaque to AHP). + Status *string `json:"status,omitempty"` + // Renderer-targeted address for the opaque canvas content — either a + // directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) + // or a channel-served `ahp-canvas-content://` address the + // renderer resolves over this channel with `canvasReadResource`. The + // renderer dispatches on the scheme and enforces its URL policy. See + // {@link /specification/canvas-channel | Canvas Channel}. + Url *string `json:"url,omitempty"` + // Whether this instance's provider is currently available. + Availability CanvasAvailability `json:"availability"` + // Which provider owns the callbacks (`canvasOpen` / … ) for this instance. + Provider CanvasProviderSource `json:"provider"` +} + // ─── Discriminated Unions ───────────────────────────────────────────── // ResponsePart is a single part of a response stream (text, tool call, reasoning, content reference). @@ -4150,6 +4336,66 @@ func (u SessionInputRequest) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// CanvasProviderSource identifies where a canvas declaration came from — used for request routing and cleanup when its provider disconnects. +type CanvasProviderSource struct { + Value isCanvasProviderSource +} + +// isCanvasProviderSource is the marker interface implemented by every +// concrete variant of CanvasProviderSource. +type isCanvasProviderSource interface{ isCanvasProviderSource() } + +func (*CanvasServerProviderSource) isCanvasProviderSource() {} +func (*CanvasClientProviderSource) isCanvasProviderSource() {} + +// CanvasProviderSourceUnknown carries an unrecognized CanvasProviderSource variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. +type CanvasProviderSourceUnknown struct { + Raw json.RawMessage +} + +func (*CanvasProviderSourceUnknown) isCanvasProviderSource() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *CanvasProviderSource) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "server": + var value CanvasServerProviderSource + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "client": + var value CanvasClientProviderSource + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &CanvasProviderSourceUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u CanvasProviderSource) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*CanvasProviderSourceUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + // ChatOrigin describes how a chat came into existence. type ChatOrigin struct { Value isChatOrigin @@ -4232,10 +4478,10 @@ func (o ChatOrigin) MarshalJSON() ([]byte, error) { } // SnapshotState is the state payload of a snapshot — root, session, -// chat, terminal, changeset, resource-watch, or annotations state. The active -// variant is chosen by which pointer field is non-nil; UnmarshalJSON probes -// for required fields in the canonical order -// (session → chat → terminal → changeset → resourceWatch → annotations → root). +// chat, terminal, changeset, resource-watch, canvas, or annotations state. The +// active variant is chosen by which pointer field is non-nil; UnmarshalJSON +// probes for required fields in the canonical order +// (session → chat → terminal → changeset → resourceWatch → canvas → annotations → root). type SnapshotState struct { Root *RootState `json:"-"` Session *SessionState `json:"-"` @@ -4243,6 +4489,7 @@ type SnapshotState struct { Terminal *TerminalState `json:"-"` Changeset *ChangesetState `json:"-"` ResourceWatch *ResourceWatchState `json:"-"` + Canvas *CanvasState `json:"-"` Annotations *AnnotationsState `json:"-"` } @@ -4259,6 +4506,8 @@ func (s SnapshotState) MarshalJSON() ([]byte, error) { return json.Marshal(s.Changeset) case s.ResourceWatch != nil: return json.Marshal(s.ResourceWatch) + case s.Canvas != nil: + return json.Marshal(s.Canvas) case s.Annotations != nil: return json.Marshal(s.Annotations) case s.Root != nil: @@ -4307,6 +4556,12 @@ func (s *SnapshotState) UnmarshalJSON(data []byte) error { return err } s.ResourceWatch = &v + case containsAll(probe, "canvasId", "provider"): + var v CanvasState + if err := json.Unmarshal(data, &v); err != nil { + return err + } + s.Canvas = &v case containsAll(probe, "annotations"): var v AnnotationsState if err := json.Unmarshal(data, &v); err != nil { diff --git a/clients/go/ahptypes/version.generated.go b/clients/go/ahptypes/version.generated.go index 8d270be4..320619c1 100644 --- a/clients/go/ahptypes/version.generated.go +++ b/clients/go/ahptypes/version.generated.go @@ -6,12 +6,13 @@ package ahptypes // ProtocolVersion is the current protocol version (SemVer // MAJOR.MINOR.PATCH) that this generated source speaks. -const ProtocolVersion = "0.5.1" +const ProtocolVersion = "0.6.0" // supportedProtocolVersions backs [SupportedProtocolVersions] — held // in an unexported slice so callers cannot accidentally mutate the // shared backing array. var supportedProtocolVersions = []string{ + "0.6.0", "0.5.1", "0.5.0", } diff --git a/clients/go/release-metadata.json b/clients/go/release-metadata.json index c8eed75a..37023344 100644 --- a/clients/go/release-metadata.json +++ b/clients/go/release-metadata.json @@ -2,6 +2,7 @@ "client": "go", "packageVersion": "0.5.0", "supportedProtocolVersions": [ + "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index eb5c051e..9643502b 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -39,6 +39,16 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump lifecycle state. - Optional `model` and `tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +- Canvas channel support: the per-instance `CanvasState` plus the + `CanvasUpdatedAction`, `CanvasCloseRequestedAction`, and `CanvasMessageAction` + actions, the `SessionCanvasesChangedAction` / `SessionOpenCanvasesChangedAction` + session actions, and the canvas discovery types (`SessionCanvasDeclaration`, + `ClientCanvasDeclaration`, `OpenCanvasRef`, `CanvasProviderSource`) on + `SessionState.canvases` / `SessionState.openCanvases`. Adds the + `ClientCapabilities.canvas` capability, the `canvasOpen` / `canvasInvokeAction` + / `canvasClose` / `canvasReadResource` methods, and the `CanvasProviderError` + error. The session reducer replaces the canvas registry/catalogue and the + canvas reducer sparse-merges `canvas/updated`. ### Removed diff --git a/clients/kotlin/release-metadata.json b/clients/kotlin/release-metadata.json index d9e0f413..f56205b0 100644 --- a/clients/kotlin/release-metadata.json +++ b/clients/kotlin/release-metadata.json @@ -2,6 +2,7 @@ "client": "kotlin", "packageVersion": "0.5.0", "supportedProtocolVersions": [ + "0.6.0", "0.5.1", "0.5.0" ] 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 85b5ccc1..e607bf6f 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -537,6 +537,10 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat is StateActionSessionServerToolsChanged -> state.copy(serverTools = action.value.tools) + is StateActionSessionCanvasesChanged -> state.copy(canvases = action.value.canvases) + + is StateActionSessionOpenCanvasesChanged -> state.copy(openCanvases = action.value.openCanvases) + is StateActionSessionActiveClientSet -> { val client = action.value.activeClient val idx = state.activeClients.indexOfFirst { it.clientId == client.clientId } @@ -1455,3 +1459,24 @@ public fun resourceWatchReducer(state: ResourceWatchState, action: StateAction): is StateActionResourceWatchChanged -> state else -> state } + +/** + * Pure reducer for a [CanvasState]. `canvas/updated` is a sparse merge — + * a presented field (title, status, url, availability) overwrites the + * corresponding [CanvasState] field while an absent field preserves the + * current value. `canvas/closeRequested` and `canvas/message` are pure + * client→host signals the host acts on out of band, mirroring how + * `terminal/input` is side-effect-only, so they return [state] unchanged. + * Actions belonging to other channels (or unknown variants) are no-ops. + */ +public fun canvasReducer(state: CanvasState, action: StateAction): CanvasState = when (action) { + is StateActionCanvasUpdated -> state.copy( + title = action.value.title ?: state.title, + status = action.value.status ?: state.status, + url = action.value.url ?: state.url, + availability = action.value.availability ?: state.availability, + ) + is StateActionCanvasCloseRequested -> state + is StateActionCanvasMessage -> state + else -> state +} 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 0bfff3a8..3fa2af3e 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 @@ -175,7 +175,17 @@ enum class ActionType { @SerialName("terminal/commandFinished") TERMINAL_COMMAND_FINISHED, @SerialName("resourceWatch/changed") - RESOURCE_WATCH_CHANGED + RESOURCE_WATCH_CHANGED, + @SerialName("session/canvasesChanged") + SESSION_CANVASES_CHANGED, + @SerialName("session/openCanvasesChanged") + SESSION_OPEN_CANVASES_CHANGED, + @SerialName("canvas/updated") + CANVAS_UPDATED, + @SerialName("canvas/closeRequested") + CANVAS_CLOSE_REQUESTED, + @SerialName("canvas/message") + CANVAS_MESSAGE } // ─── Action Infrastructure ────────────────────────────────────────────────── @@ -773,6 +783,24 @@ data class SessionServerToolsChangedAction( val tools: List ) +@Serializable +data class SessionCanvasesChangedAction( + val type: ActionType, + /** + * Updated canvas registry (full replacement). + */ + val canvases: List +) + +@Serializable +data class SessionOpenCanvasesChangedAction( + val type: ActionType, + /** + * Updated open-instance catalogue (full replacement). + */ + val openCanvases: List +) + @Serializable data class SessionActiveClientSetAction( val type: ActionType, @@ -1291,6 +1319,41 @@ data class ResourceWatchChangedAction( val changes: JsonElement ) +@Serializable +data class CanvasUpdatedAction( + val type: ActionType, + /** + * New title. Absent preserves the current title. + */ + val title: String? = null, + /** + * New provider-defined status. Absent preserves the current status. + */ + val status: String? = null, + /** + * New content address. Absent preserves the current url. + */ + val url: String? = null, + /** + * New availability. Absent preserves the current availability. + */ + val availability: CanvasAvailability? = null +) + +@Serializable +data class CanvasCloseRequestedAction( + val type: ActionType +) + +@Serializable +data class CanvasMessageAction( + val type: ActionType, + /** + * Opaque, provider-defined message payload. + */ + val payload: JsonElement +) + // ─── Partial Summary Types ────────────────────────────────────────────────── @Serializable @@ -1381,6 +1444,8 @@ sealed interface StateAction @JvmInline value class StateActionSessionActivityChanged(val value: SessionActivityChangedAction) : StateAction @JvmInline value class StateActionSessionChangesetsChanged(val value: SessionChangesetsChangedAction) : StateAction @JvmInline value class StateActionSessionServerToolsChanged(val value: SessionServerToolsChangedAction) : StateAction +@JvmInline value class StateActionSessionCanvasesChanged(val value: SessionCanvasesChangedAction) : StateAction +@JvmInline value class StateActionSessionOpenCanvasesChanged(val value: SessionOpenCanvasesChangedAction) : StateAction @JvmInline value class StateActionSessionActiveClientSet(val value: SessionActiveClientSetAction) : StateAction @JvmInline value class StateActionSessionActiveClientRemoved(val value: SessionActiveClientRemovedAction) : StateAction @JvmInline value class StateActionSessionInputNeededSet(val value: SessionInputNeededSetAction) : StateAction @@ -1426,6 +1491,9 @@ sealed interface StateAction @JvmInline value class StateActionTerminalCommandExecuted(val value: TerminalCommandExecutedAction) : StateAction @JvmInline value class StateActionTerminalCommandFinished(val value: TerminalCommandFinishedAction) : StateAction @JvmInline value class StateActionResourceWatchChanged(val value: ResourceWatchChangedAction) : StateAction +@JvmInline value class StateActionCanvasUpdated(val value: CanvasUpdatedAction) : StateAction +@JvmInline value class StateActionCanvasCloseRequested(val value: CanvasCloseRequestedAction) : StateAction +@JvmInline value class StateActionCanvasMessage(val value: CanvasMessageAction) : StateAction @JvmInline value class StateActionUnknown(val raw: JsonObject) : StateAction internal object StateActionSerializer : KSerializer { @@ -1471,6 +1539,8 @@ internal object StateActionSerializer : KSerializer { "session/activityChanged" -> StateActionSessionActivityChanged(input.json.decodeFromJsonElement(SessionActivityChangedAction.serializer(), element)) "session/changesetsChanged" -> StateActionSessionChangesetsChanged(input.json.decodeFromJsonElement(SessionChangesetsChangedAction.serializer(), element)) "session/serverToolsChanged" -> StateActionSessionServerToolsChanged(input.json.decodeFromJsonElement(SessionServerToolsChangedAction.serializer(), element)) + "session/canvasesChanged" -> StateActionSessionCanvasesChanged(input.json.decodeFromJsonElement(SessionCanvasesChangedAction.serializer(), element)) + "session/openCanvasesChanged" -> StateActionSessionOpenCanvasesChanged(input.json.decodeFromJsonElement(SessionOpenCanvasesChangedAction.serializer(), element)) "session/activeClientSet" -> StateActionSessionActiveClientSet(input.json.decodeFromJsonElement(SessionActiveClientSetAction.serializer(), element)) "session/activeClientRemoved" -> StateActionSessionActiveClientRemoved(input.json.decodeFromJsonElement(SessionActiveClientRemovedAction.serializer(), element)) "session/inputNeededSet" -> StateActionSessionInputNeededSet(input.json.decodeFromJsonElement(SessionInputNeededSetAction.serializer(), element)) @@ -1516,6 +1586,9 @@ internal object StateActionSerializer : KSerializer { "terminal/commandExecuted" -> StateActionTerminalCommandExecuted(input.json.decodeFromJsonElement(TerminalCommandExecutedAction.serializer(), element)) "terminal/commandFinished" -> StateActionTerminalCommandFinished(input.json.decodeFromJsonElement(TerminalCommandFinishedAction.serializer(), element)) "resourceWatch/changed" -> StateActionResourceWatchChanged(input.json.decodeFromJsonElement(ResourceWatchChangedAction.serializer(), element)) + "canvas/updated" -> StateActionCanvasUpdated(input.json.decodeFromJsonElement(CanvasUpdatedAction.serializer(), element)) + "canvas/closeRequested" -> StateActionCanvasCloseRequested(input.json.decodeFromJsonElement(CanvasCloseRequestedAction.serializer(), element)) + "canvas/message" -> StateActionCanvasMessage(input.json.decodeFromJsonElement(CanvasMessageAction.serializer(), element)) else -> StateActionUnknown(obj) } } @@ -1554,6 +1627,8 @@ internal object StateActionSerializer : KSerializer { is StateActionSessionActivityChanged -> output.json.encodeToJsonElement(SessionActivityChangedAction.serializer(), value.value) is StateActionSessionChangesetsChanged -> output.json.encodeToJsonElement(SessionChangesetsChangedAction.serializer(), value.value) is StateActionSessionServerToolsChanged -> output.json.encodeToJsonElement(SessionServerToolsChangedAction.serializer(), value.value) + is StateActionSessionCanvasesChanged -> output.json.encodeToJsonElement(SessionCanvasesChangedAction.serializer(), value.value) + is StateActionSessionOpenCanvasesChanged -> output.json.encodeToJsonElement(SessionOpenCanvasesChangedAction.serializer(), value.value) is StateActionSessionActiveClientSet -> output.json.encodeToJsonElement(SessionActiveClientSetAction.serializer(), value.value) is StateActionSessionActiveClientRemoved -> output.json.encodeToJsonElement(SessionActiveClientRemovedAction.serializer(), value.value) is StateActionSessionInputNeededSet -> output.json.encodeToJsonElement(SessionInputNeededSetAction.serializer(), value.value) @@ -1599,6 +1674,9 @@ internal object StateActionSerializer : KSerializer { is StateActionTerminalCommandExecuted -> output.json.encodeToJsonElement(TerminalCommandExecutedAction.serializer(), value.value) is StateActionTerminalCommandFinished -> output.json.encodeToJsonElement(TerminalCommandFinishedAction.serializer(), value.value) is StateActionResourceWatchChanged -> output.json.encodeToJsonElement(ResourceWatchChangedAction.serializer(), value.value) + is StateActionCanvasUpdated -> output.json.encodeToJsonElement(CanvasUpdatedAction.serializer(), value.value) + is StateActionCanvasCloseRequested -> output.json.encodeToJsonElement(CanvasCloseRequestedAction.serializer(), value.value) + is StateActionCanvasMessage -> output.json.encodeToJsonElement(CanvasMessageAction.serializer(), value.value) is StateActionUnknown -> value.raw } output.encodeJsonElement(element) 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 26402250..27ceeaa5 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 @@ -195,7 +195,19 @@ data class ClientCapabilities( * capability is declared. Clients that omit it MUST treat * App-bearing tool calls as ordinary MCP tool calls. */ - val mcpApps: Map? = null + val mcpApps: Map? = null, + /** + * Client can render canvases and host client-declared canvas providers — it + * can render an opaque canvas URL in an isolated surface, and it can answer + * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + * it declares via {@link SessionActiveClient.canvasProviders}. + * + * Hosts SHOULD only populate {@link SessionState.canvases} / + * {@link SessionState.openCanvases} and only route canvas requests to a + * client that declared this capability. Clients that omit it see no canvas + * surface. See {@link /specification/canvas-channel | Canvas Channel}. + */ + val canvas: Map? = null ) @Serializable @@ -1118,6 +1130,142 @@ data class ChangesetOperationFollowUp( val external: Boolean? = null ) +@Serializable +data class CanvasOpenParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Provider-local canvas id to open. + */ + val canvasId: String, + /** + * Owning provider id. + */ + val extensionId: String, + /** + * Caller-minted handle for the new instance. + */ + val instanceId: String, + /** + * Open input, validated by the provider against its declared schema. + */ + val input: Map? = null +) + +@Serializable +data class CanvasOpenResult( + /** + * Initial content address for the instance (see {@link CanvasState.url}). + */ + val url: String? = null, + /** + * Initial title. + */ + val title: String? = null, + /** + * Initial provider-defined status. + */ + val status: String? = null +) + +@Serializable +data class CanvasInvokeActionParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Instance handle the action targets. + */ + val instanceId: String, + /** + * Provider-local canvas id of the instance. + */ + val canvasId: String, + /** + * Owning provider id. + */ + val extensionId: String, + /** + * Declared action name to invoke. + */ + val actionName: String, + /** + * Action input, validated by the provider against its declared schema. + */ + val input: Map? = null +) + +@Serializable +data class CanvasInvokeActionResult( + /** + * Opaque, provider-defined return value. + */ + val value: JsonElement? = null +) + +@Serializable +data class CanvasCloseParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Instance handle to close. + */ + val instanceId: String, + /** + * Provider-local canvas id of the instance. + */ + val canvasId: String, + /** + * Owning provider id. + */ + val extensionId: String +) + +@Serializable +data class CanvasReadResourceParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * An `ahp-canvas-content://` content URI to read. + */ + val uri: String +) + +@Serializable +data class CanvasReadResourceResult( + /** + * The resolved content parts, wrapped for forward compatibility. + */ + val contents: List +) + +@Serializable +data class CanvasResourceContent( + /** + * The content URI this part resolves. + */ + val uri: String, + /** + * MIME type of the content, when known. + */ + val mimeType: String? = null, + /** + * UTF-8 text content, for text payloads. + */ + val text: String? = null, + /** + * Base64-encoded content, for binary payloads. + */ + val blob: String? = null +) + // ─── ReconnectResult Union ────────────────────────────────────────────────── @Serializable(with = ReconnectResultSerializer::class) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt index 9aeea82f..8d9e0e43 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt @@ -57,6 +57,10 @@ object AhpErrorCodes { const val PERMISSION_DENIED: Int = -32009 /** The target resource already exists and the operation does not allow overwriting */ const val ALREADY_EXISTS: Int = -32010 + /** An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state */ + const val CONFLICT: Int = -32011 + /** A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` */ + const val CANVAS_PROVIDER_ERROR: Int = -32012 } // ─── Error Detail Payloads ────────────────────────────────────────────────── @@ -90,3 +94,15 @@ data class UnsupportedProtocolVersionErrorData( */ val supportedVersions: List ) + +@Serializable +data class CanvasProviderErrorData( + /** + * Provider-defined error code identifying the failure. + */ + val code: String, + /** + * Human-readable error message. + */ + val message: String +) 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 4ca2b7de..771beed4 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 @@ -706,6 +706,42 @@ enum class ResourceChangeType { DELETED } +/** + * Availability of a canvas provider or open instance. + */ +@Serializable +enum class CanvasAvailability { + /** + * The provider is connected and can service requests for this canvas. + */ + @SerialName("ready") + READY, + /** + * The provider is temporarily unavailable (for example, a client provider + * that disconnected). The entry is retained so it can be restored when the + * provider reconnects; in-flight requests fail until then. + */ + @SerialName("stale") + STALE +} + +/** + * Discriminant for {@link CanvasProviderSource}. + */ +@Serializable +enum class CanvasProviderKind { + /** + * The canvas is provided by the host itself. + */ + @SerialName("server") + SERVER, + /** + * The canvas is provided by a connected client. + */ + @SerialName("client") + CLIENT +} + // ─── State Types ──────────────────────────────────────────────────────────── @Serializable @@ -1332,6 +1368,29 @@ data class SessionState( * once the underlying request resolves. */ val inputNeeded: List? = null, + /** + * Aggregated canvas registry currently exposed to the agent — the union of + * every connected provider (server-side and client-declared). Each entry + * describes a canvas the agent can open; the host folds + * {@link SessionActiveClient.canvasProviders | client-declared providers} + * into this list alongside its own server-side providers. + * + * Full-replacement via `session/canvasesChanged`. Populated only when at + * least one connected client declared {@link ClientCapabilities.canvas}; + * absent for sessions with no canvas surface. See + * {@link /specification/canvas-channel | Canvas Channel}. + */ + val canvases: List? = null, + /** + * Lightweight catalogue of currently-open canvas instances. Each entry + * carries the instance's `ahp-canvas:/` channel URI so a subscriber can + * subscribe to the full {@link CanvasState} and render it — analogous to how + * {@link RootState.terminals} catalogues live terminals whose full state + * lives on each terminal channel. + * + * Full-replacement via `session/openCanvasesChanged`. + */ + val openCanvases: List? = null, /** * Additional provider-specific metadata for this session. * @@ -1365,7 +1424,20 @@ data class SessionActiveClient( * plugins in memory and rely on the host to expand them into concrete * children inside {@link SessionState.customizations}. */ - val customizations: List? = null + val customizations: List? = null, + /** + * Canvas declarations this client contributes as a provider. Published + * atomically with the rest of the active-client entry via + * `session/activeClientSet` — exactly like {@link tools} — so there is no + * separate canvas-provider change action. The host folds these into + * {@link SessionState.canvases} with + * {@link SessionCanvasDeclaration.source | `source`} set to + * `{ kind: 'client', clientId }` and routes + * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back + * to this client. Only meaningful for a client that declared + * {@link ClientCapabilities.canvas}. + */ + val canvasProviders: List? = null ) @Serializable @@ -3998,6 +4070,185 @@ data class ResourceChange( val type: ResourceChangeType ) +@Serializable +data class SessionCanvasAction( + /** + * Action name, unique within the owning `(extensionId, canvasId)`. + */ + val name: String, + /** + * Human-readable description of what the action does. + */ + val description: String? = null, + /** + * JSON Schema for the action's input. Opaque to AHP; mirrors the + * {@link ToolDefinition.inputSchema} shape. + */ + val inputSchema: Map? = null +) + +@Serializable +data class SessionCanvasDeclaration( + /** + * Owning provider id. Stable across declarations and instances. + */ + val extensionId: String, + /** + * Human-readable provider name. + */ + val extensionName: String? = null, + /** + * Provider-local canvas id. Unique within `extensionId`. + */ + val canvasId: String, + /** + * Human-readable canvas name. + */ + val displayName: String, + /** + * Human-readable description of the canvas. + */ + val description: String, + /** + * JSON Schema for the canvas's open input. Opaque to AHP; mirrors the + * {@link ToolDefinition.inputSchema} shape. + */ + val inputSchema: Map? = null, + /** + * Actions this canvas exposes to the agent. + */ + val actions: List? = null, + /** + * Where the declaration came from — for routing and cleanup. + */ + val source: CanvasProviderSource +) + +@Serializable +data class ClientCanvasDeclaration( + /** + * Provider-local canvas id, unique within the publishing client. + */ + val canvasId: String, + /** + * Human-readable canvas name. + */ + val displayName: String, + /** + * Human-readable description of the canvas. + */ + val description: String, + /** + * JSON Schema for the canvas's open input. Opaque to AHP. + */ + val inputSchema: Map? = null, + /** + * Actions this canvas exposes to the agent. + */ + val actions: List? = null +) + +@Serializable +data class OpenCanvasRef( + /** + * Server-assigned instance handle, unique within the session. + */ + val instanceId: String, + /** + * The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load + * the full {@link CanvasState}. + */ + val channel: String, + /** + * Provider-local canvas id this instance was opened from. + */ + val canvasId: String, + /** + * Owning provider id. + */ + val extensionId: String, + /** + * Human-readable provider name. + */ + val extensionName: String? = null, + /** + * Current instance title, mirrored from {@link CanvasState.title}. + */ + val title: String? = null, + /** + * Whether the instance's provider is currently available. + */ + val availability: CanvasAvailability +) + +@Serializable +data class CanvasServerProviderSource( + val kind: CanvasProviderKind +) + +@Serializable +data class CanvasClientProviderSource( + val kind: CanvasProviderKind, + /** + * `clientId` of the providing client (matches `initialize`). + */ + val clientId: String +) + +@Serializable +data class CanvasState( + /** + * Server-assigned instance handle, unique within the session. + */ + val instanceId: String, + /** + * Provider-local canvas id this instance was opened from. + */ + val canvasId: String, + /** + * Owning provider id. + */ + val extensionId: String, + /** + * Human-readable provider name. + */ + val extensionName: String? = null, + /** + * Human-readable canvas name. + */ + val displayName: String? = null, + /** + * Input the agent supplied when opening the instance. Retained so the + * instance can be resumed or rebound after a reconnect. + */ + val input: Map? = null, + /** + * Current instance title. + */ + val title: String? = null, + /** + * Provider-defined status string (opaque to AHP). + */ + val status: String? = null, + /** + * Renderer-targeted address for the opaque canvas content — either a + * directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) + * or a channel-served `ahp-canvas-content://` address the + * renderer resolves over this channel with `canvasReadResource`. The + * renderer dispatches on the scheme and enforces its URL policy. See + * {@link /specification/canvas-channel | Canvas Channel}. + */ + val url: String? = null, + /** + * Whether this instance's provider is currently available. + */ + val availability: CanvasAvailability, + /** + * Which provider owns the callbacks (`canvasOpen` / … ) for this instance. + */ + val provider: CanvasProviderSource +) + // ─── Discriminated Unions ─────────────────────────────────────────────────── @Serializable(with = ChatOriginSerializer::class) @@ -4895,6 +5146,55 @@ internal object SessionInputRequestSerializer : KSerializer } } +@Serializable(with = CanvasProviderSourceSerializer::class) +sealed interface CanvasProviderSource + +@JvmInline +value class CanvasProviderSourceServer(val value: CanvasServerProviderSource) : CanvasProviderSource +@JvmInline +value class CanvasProviderSourceClient(val value: CanvasClientProviderSource) : CanvasProviderSource +/** + * Forward-compat catch-all for unknown CanvasProviderSource discriminators. + * + * Older clients may receive newer wire variants they don't recognise; capturing + * the raw `JsonObject` lets such payloads round-trip through the client unchanged. + * Reducers handle this variant conservatively on a per-union basis (typically + * as a no-op, but see `Reducers.kt` for the exact treatment). + */ +@JvmInline +value class CanvasProviderSourceUnknown(val raw: JsonObject) : CanvasProviderSource + +internal object CanvasProviderSourceSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("CanvasProviderSource") + + override fun deserialize(decoder: Decoder): CanvasProviderSource { + val input = decoder as? JsonDecoder + ?: error("CanvasProviderSource can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for CanvasProviderSource") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: return CanvasProviderSourceUnknown(obj) + return when (discriminant) { + "server" -> CanvasProviderSourceServer(input.json.decodeFromJsonElement(CanvasServerProviderSource.serializer(), element)) + "client" -> CanvasProviderSourceClient(input.json.decodeFromJsonElement(CanvasClientProviderSource.serializer(), element)) + else -> CanvasProviderSourceUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: CanvasProviderSource) { + val output = encoder as? JsonEncoder + ?: error("CanvasProviderSource can only be serialized to JSON") + val element: JsonElement = when (value) { + is CanvasProviderSourceServer -> output.json.encodeToJsonElement(CanvasServerProviderSource.serializer(), value.value) + is CanvasProviderSourceClient -> output.json.encodeToJsonElement(CanvasClientProviderSource.serializer(), value.value) + is CanvasProviderSourceUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} + @Serializable(with = ToolResultContentSerializer::class) sealed interface ToolResultContent { @JvmInline value class Text(val value: ToolResultTextContent) : ToolResultContent @@ -4954,7 +5254,7 @@ internal object ToolResultContentSerializer : KSerializer { /** * The state payload of a snapshot — root, session, chat, terminal, changeset, - * resource-watch, or annotations state. + * resource-watch, canvas, or annotations state. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @@ -4964,6 +5264,7 @@ sealed interface SnapshotState { @JvmInline value class Terminal(val value: TerminalState) : SnapshotState @JvmInline value class Changeset(val value: ChangesetState) : SnapshotState @JvmInline value class ResourceWatch(val value: ResourceWatchState) : SnapshotState + @JvmInline value class Canvas(val value: CanvasState) : SnapshotState @JvmInline value class Annotations(val value: AnnotationsState) : SnapshotState } @@ -4980,7 +5281,8 @@ internal object SnapshotStateSerializer : KSerializer { // Try the most distinctive shape first. SessionState has required // `lifecycle`; ChatState has required `turns`; ChangesetState has // required `status` + `files`; ResourceWatchState has required - // `root` + `recursive`; AnnotationsState has required `annotations` + // `root` + `recursive`; CanvasState has required `canvasId` + + // `provider`; AnnotationsState has required `annotations` // (checked after session, whose optional annotations summary reuses the // key); TerminalState has required `content`; RootState is the // catch-all. @@ -4991,6 +5293,8 @@ internal object SnapshotStateSerializer : KSerializer { SnapshotState.Changeset(input.json.decodeFromJsonElement(ChangesetState.serializer(), element)) obj.containsKey("root") && obj.containsKey("recursive") -> SnapshotState.ResourceWatch(input.json.decodeFromJsonElement(ResourceWatchState.serializer(), element)) + obj.containsKey("canvasId") && obj.containsKey("provider") -> + SnapshotState.Canvas(input.json.decodeFromJsonElement(CanvasState.serializer(), element)) obj.containsKey("annotations") -> SnapshotState.Annotations(input.json.decodeFromJsonElement(AnnotationsState.serializer(), element)) obj.containsKey("content") -> @@ -5009,6 +5313,7 @@ internal object SnapshotStateSerializer : KSerializer { is SnapshotState.Terminal -> output.json.encodeToJsonElement(TerminalState.serializer(), value.value) is SnapshotState.Changeset -> output.json.encodeToJsonElement(ChangesetState.serializer(), value.value) is SnapshotState.ResourceWatch -> output.json.encodeToJsonElement(ResourceWatchState.serializer(), value.value) + is SnapshotState.Canvas -> output.json.encodeToJsonElement(CanvasState.serializer(), value.value) is SnapshotState.Annotations -> output.json.encodeToJsonElement(AnnotationsState.serializer(), value.value) } output.encodeJsonElement(element) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt index c1a4c3cf..666c8aab 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt @@ -5,7 +5,7 @@ package com.microsoft.agenthostprotocol.generated /** * Current protocol version (SemVer `MAJOR.MINOR.PATCH`). */ -public const val PROTOCOL_VERSION: String = "0.5.1" +public const val PROTOCOL_VERSION: String = "0.6.0" /** * Every protocol version this library is willing to negotiate, ordered @@ -16,6 +16,7 @@ public const val PROTOCOL_VERSION: String = "0.5.1" * protocol versions if the host doesn't accept the newest one. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( + "0.6.0", "0.5.1", "0.5.0", ) diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt index 37cf486b..84b5f311 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt @@ -3,6 +3,7 @@ package com.microsoft.agenthostprotocol import com.microsoft.agenthostprotocol.generated.ChatState import com.microsoft.agenthostprotocol.generated.ChangesetState import com.microsoft.agenthostprotocol.generated.AnnotationsState +import com.microsoft.agenthostprotocol.generated.CanvasState import com.microsoft.agenthostprotocol.generated.ResourceWatchState import com.microsoft.agenthostprotocol.generated.RootState import com.microsoft.agenthostprotocol.generated.SessionState @@ -216,6 +217,18 @@ class FixtureDrivenReducerTest { }, ) + "canvas" -> compareFixture( + file = file, + initial = initial, + expected = expected, + serializer = CanvasState.serializer(), + run = { state -> + var s = state + for (action in actions) s = canvasReducer(s, action) + s + }, + ) + else -> fail("${file.name}: unsupported reducer '$reducer'") } } diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 68deb44e..9d21090c 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -40,6 +40,16 @@ matching `## [X.Y.Z]` heading is missing from this file. lifecycle state. - Optional `model` and `tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +- Canvas channel support: the per-instance `CanvasState` plus the + `StateAction::CanvasUpdated` / `StateAction::CanvasCloseRequested` / + `StateAction::CanvasMessage` actions, the `StateAction::SessionCanvasesChanged` + / `StateAction::SessionOpenCanvasesChanged` session actions, and the canvas + discovery types (`SessionCanvasDeclaration`, `ClientCanvasDeclaration`, + `OpenCanvasRef`, `CanvasProviderSource`) on `SessionState.canvases` / + `SessionState.open_canvases`. Adds the `ClientCapabilities.canvas` capability, + the `canvasOpen` / `canvasInvokeAction` / `canvasClose` / `canvasReadResource` + methods, and the `CanvasProviderError` error. The session reducer replaces the + canvas registry/catalogue and the canvas reducer sparse-merges `canvas/updated`. ### Changed diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 2e79251e..539427a1 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -13,13 +13,14 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; #[allow(unused_imports)] use crate::state::{ - AgentInfo, AgentSelection, Annotation, AnnotationEntry, Changeset, ChangesetFile, - ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, + AgentInfo, AgentSelection, Annotation, AnnotationEntry, CanvasAvailability, Changeset, + ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, ConfirmationOption, Customization, ErrorInfo, McpServerState, Message, ModelSelection, - PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, - TerminalInfo, TextRange, ToolCallCancellationReason, ToolCallConfirmationReason, - ToolCallContributor, ToolCallResult, ToolDefinition, ToolResultContent, UsageInfo, + OpenCanvasRef, PendingMessageKind, ResponsePart, SessionActiveClient, SessionCanvasDeclaration, + SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, + ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolDefinition, + ToolResultContent, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -177,6 +178,16 @@ pub enum ActionType { TerminalCommandFinished, #[serde(rename = "resourceWatch/changed")] ResourceWatchChanged, + #[serde(rename = "session/canvasesChanged")] + SessionCanvasesChanged, + #[serde(rename = "session/openCanvasesChanged")] + SessionOpenCanvasesChanged, + #[serde(rename = "canvas/updated")] + CanvasUpdated, + #[serde(rename = "canvas/closeRequested")] + CanvasCloseRequested, + #[serde(rename = "canvas/message")] + CanvasMessage, } // ─── Action Envelope ───────────────────────────────────────────────── @@ -781,6 +792,31 @@ pub struct SessionServerToolsChangedAction { pub tools: Vec, } +/// The aggregated canvas registry for this session changed. +/// +/// Full-replacement semantics: the `canvases` array replaces +/// {@link SessionState.canvases} entirely, mirroring +/// `session/serverToolsChanged`. The host republishes the union of every +/// connected provider (server-side and client-declared) whenever it changes. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCanvasesChangedAction { + /// Updated canvas registry (full replacement). + pub canvases: Vec, +} + +/// The catalogue of open canvas instances for this session changed. +/// +/// Full-replacement semantics: the `openCanvases` array replaces +/// {@link SessionState.openCanvases} entirely. The host republishes the +/// catalogue as instances open and close. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionOpenCanvasesChangedAction { + /// Updated open-instance catalogue (full replacement). + pub open_canvases: Vec, +} + /// An active client for this session was added or updated. /// /// Upsert semantics keyed by {@link SessionActiveClient.clientId | `clientId`}: @@ -1461,6 +1497,58 @@ pub struct ResourceWatchChangedAction { pub changes: AnyValue, } +/// The canvas instance's presentation state changed. +/// +/// Sparse-merge semantics: each present field overwrites the corresponding +/// {@link CanvasState} field, and an absent field preserves the current value. +/// There is no clear-to-absent via this action — that three-state distinction +/// cannot survive JSON transport uniformly across languages, so a provider that +/// needs to reset a field re-publishes it, and a full reset arrives as a fresh +/// {@link CanvasState} snapshot on (re)subscribe. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CanvasUpdatedAction { + /// New title. Absent preserves the current title. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// New provider-defined status. Absent preserves the current status. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, + /// New content address. Absent preserves the current url. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + /// New availability. Absent preserves the current availability. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub availability: Option, +} + +/// The user asked to close this canvas (e.g. hit the ✕ on the surface). +/// +/// A pure client→host signal with a no-op reducer, mirroring how +/// `terminal/input` is a side-effect-only client action. The host runs the +/// close flow in response — resolving `canvasClose` against the provider and +/// dropping the instance from {@link SessionState.openCanvases} — rather than +/// the reducer mutating channel state. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasCloseRequestedAction {} + +/// An opaque message relayed between the rendered canvas View and the +/// instance's provider — the relay-carried analogue of a `postMessage` bridge. +/// +/// Bidirectional: a client dispatches it to carry a View→provider message, and +/// the host emits it to carry a provider→View message (routed to the provider +/// resolved for the instance, or handled host-internally for a server-side +/// provider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it +/// is a pure signal with a no-op reducer, so it never bloats channel state. See +/// {@link /specification/canvas-channel | Canvas Channel}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasMessageAction { + /// Opaque, provider-defined message payload. + pub payload: AnyValue, +} + // ─── Partial Summaries ──────────────────────────────────────────────── /// Partial equivalent of ChatSummary — every field is optional for delta updates. @@ -1569,6 +1657,10 @@ pub enum StateAction { SessionChangesetsChanged(SessionChangesetsChangedAction), #[serde(rename = "session/serverToolsChanged")] SessionServerToolsChanged(SessionServerToolsChangedAction), + #[serde(rename = "session/canvasesChanged")] + SessionCanvasesChanged(SessionCanvasesChangedAction), + #[serde(rename = "session/openCanvasesChanged")] + SessionOpenCanvasesChanged(SessionOpenCanvasesChangedAction), #[serde(rename = "session/activeClientSet")] SessionActiveClientSet(SessionActiveClientSetAction), #[serde(rename = "session/activeClientRemoved")] @@ -1657,6 +1749,12 @@ pub enum StateAction { TerminalCommandFinished(TerminalCommandFinishedAction), #[serde(rename = "resourceWatch/changed")] ResourceWatchChanged(ResourceWatchChangedAction), + #[serde(rename = "canvas/updated")] + CanvasUpdated(CanvasUpdatedAction), + #[serde(rename = "canvas/closeRequested")] + CanvasCloseRequested(CanvasCloseRequestedAction), + #[serde(rename = "canvas/message")] + CanvasMessage(CanvasMessageAction), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index b55dd88f..5acc0d30 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -183,6 +183,17 @@ pub struct ClientCapabilities { /// App-bearing tool calls as ordinary MCP tool calls. #[serde(default, skip_serializing_if = "Option::is_none")] pub mcp_apps: Option, + /// Client can render canvases and host client-declared canvas providers — it + /// can render an opaque canvas URL in an isolated surface, and it can answer + /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + /// it declares via {@link SessionActiveClient.canvasProviders}. + /// + /// Hosts SHOULD only populate {@link SessionState.canvases} / + /// {@link SessionState.openCanvases} and only route canvas requests to a + /// client that declared this capability. Clients that omit it see no canvas + /// surface. See {@link /specification/canvas-channel | Canvas Channel}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub canvas: Option, } /// Re-establishes a dropped connection. The server replays missed actions or @@ -1146,6 +1157,151 @@ pub struct ChangesetOperationFollowUp { pub external: Option, } +/// Opens a canvas instance against its provider. +/// +/// Sent by the host to the client that declared the target canvas via +/// {@link SessionActiveClient.canvasProviders} (a client that also declared +/// {@link ClientCapabilities.canvas}). For a server-side provider the host +/// resolves the open host-internally and emits no request. The provider returns +/// the initial render target and presentation fields, which the host folds into +/// the new instance's {@link CanvasState}. +/// +/// Mirrors the `resource*` precedent: registered in `ServerCommandMap` and +/// mirrored in `CommandMap` for symmetry. A client normally never initiates it — +/// the host is not a canvas provider — and a receiver SHOULD reject a request +/// whose target is not one of its declared providers. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasOpenParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Provider-local canvas id to open. + pub canvas_id: String, + /// Owning provider id. + pub extension_id: String, + /// Caller-minted handle for the new instance. + pub instance_id: String, + /// Open input, validated by the provider against its declared schema. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input: Option, +} + +/// Result of the `canvasOpen` command. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CanvasOpenResult { + /// Initial content address for the instance (see {@link CanvasState.url}). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Initial title. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Initial provider-defined status. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Invokes one of a canvas's declared actions against its provider. +/// +/// Sent by the host to the providing client (or resolved host-internally for a +/// server-side provider) when the agent invokes a +/// {@link SessionCanvasAction | declared action} on an open instance. The +/// provider returns an opaque, provider-defined value. Registered symmetrically +/// with the rest of the provider family (see {@link CanvasOpenParams}). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasInvokeActionParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Instance handle the action targets. + pub instance_id: String, + /// Provider-local canvas id of the instance. + pub canvas_id: String, + /// Owning provider id. + pub extension_id: String, + /// Declared action name to invoke. + pub action_name: String, + /// Action input, validated by the provider against its declared schema. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input: Option, +} + +/// Result of the `canvasInvokeAction` command. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CanvasInvokeActionResult { + /// Opaque, provider-defined return value. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +/// Closes a canvas instance against its provider. +/// +/// Sent by the host to the providing client (or resolved host-internally for a +/// server-side provider) as part of the close flow — typically after a client +/// dispatches `canvas/closeRequested`. The host then drops the instance from +/// {@link SessionState.openCanvases}. Registered symmetrically with the rest of +/// the provider family (see {@link CanvasOpenParams}). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasCloseParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Instance handle to close. + pub instance_id: String, + /// Provider-local canvas id of the instance. + pub canvas_id: String, + /// Owning provider id. + pub extension_id: String, +} + +/// Reads channel-served canvas content by `ahp-canvas-content:` URI. +/// +/// A client → host request, modeled on MCP's `resources/read`. When a canvas's +/// {@link CanvasState.url} (or a sub-resource the rendered document references) +/// is an `ahp-canvas-content://` address, the renderer cannot +/// dial the host directly — for example in a relayed deployment behind a broker +/// — so it resolves the bytes over the instance's existing `ahp-canvas:/` +/// channel with this request instead of loading a network URL. The `` +/// segment of the URI identifies which canvas channel to read from. See +/// {@link /specification/canvas-channel | Canvas Channel}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasReadResourceParams { + /// Channel URI this command targets. + pub channel: Uri, + /// An `ahp-canvas-content://` content URI to read. + pub uri: String, +} + +/// Result of the `canvasReadResource` command. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasReadResourceResult { + /// The resolved content parts, wrapped for forward compatibility. + pub contents: Vec, +} + +/// One resolved piece of channel-served canvas content. +/// +/// Carries exactly one of {@link text} (text payloads) or {@link blob} +/// (base64-encoded binary payloads). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasResourceContent { + /// The content URI this part resolves. + pub uri: String, + /// MIME type of the content, when known. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// UTF-8 text content, for text payloads. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub text: Option, + /// Base64-encoded content, for binary payloads. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub blob: Option, +} + // ─── ReconnectResult Union ──────────────────────────────────────────── /// Result of the `reconnect` command. diff --git a/clients/rust/crates/ahp-types/src/errors.rs b/clients/rust/crates/ahp-types/src/errors.rs index 1394794b..2351e68b 100644 --- a/clients/rust/crates/ahp-types/src/errors.rs +++ b/clients/rust/crates/ahp-types/src/errors.rs @@ -55,6 +55,8 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. `ResourceWriteParams.if_match`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; + /// A canvas provider request (`canvasOpen`, `canvasInvokeAction`, or `canvasClose`) failed; the error `data` carries a provider-defined `{ code, message }`. + pub const CANVAS_PROVIDER_ERROR: i32 = -32012; } /// Type alias: AHP application error code. @@ -93,3 +95,14 @@ pub struct UnsupportedProtocolVersionErrorData { /// SemVer range constraint (e.g. `">=0.1.0 <0.3.0"` or `"^0.2.0"`). pub supported_versions: Vec, } + +/// Details carried in the `data` field of a `CanvasProviderError` (-32012) +/// error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasProviderErrorData { + /// Provider-defined error code identifying the failure. + pub code: String, + /// Human-readable error message. + pub message: String, +} diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 2e8b15d8..17b80697 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -555,6 +555,30 @@ pub enum ResourceChangeType { Deleted, } +/// Availability of a canvas provider or open instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CanvasAvailability { + /// The provider is connected and can service requests for this canvas. + #[serde(rename = "ready")] + Ready, + /// The provider is temporarily unavailable (for example, a client provider + /// that disconnected). The entry is retained so it can be restored when the + /// provider reconnects; in-flight requests fail until then. + #[serde(rename = "stale")] + Stale, +} + +/// Discriminant for {@link CanvasProviderSource}. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CanvasProviderKind { + /// The canvas is provided by the host itself. + #[serde(rename = "server")] + Server, + /// The canvas is provided by a connected client. + #[serde(rename = "client")] + Client, +} + // ─── Structs ────────────────────────────────────────────────────────── /// An optionally-sized icon that can be displayed in a user interface. @@ -1152,6 +1176,27 @@ pub struct SessionState { /// once the underlying request resolves. #[serde(default, skip_serializing_if = "Option::is_none")] pub input_needed: Option>, + /// Aggregated canvas registry currently exposed to the agent — the union of + /// every connected provider (server-side and client-declared). Each entry + /// describes a canvas the agent can open; the host folds + /// {@link SessionActiveClient.canvasProviders | client-declared providers} + /// into this list alongside its own server-side providers. + /// + /// Full-replacement via `session/canvasesChanged`. Populated only when at + /// least one connected client declared {@link ClientCapabilities.canvas}; + /// absent for sessions with no canvas surface. See + /// {@link /specification/canvas-channel | Canvas Channel}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub canvases: Option>, + /// Lightweight catalogue of currently-open canvas instances. Each entry + /// carries the instance's `ahp-canvas:/` channel URI so a subscriber can + /// subscribe to the full {@link CanvasState} and render it — analogous to how + /// {@link RootState.terminals} catalogues live terminals whose full state + /// lives on each terminal channel. + /// + /// Full-replacement via `session/openCanvasesChanged`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub open_canvases: Option>, /// Additional provider-specific metadata for this session. /// /// Clients MAY look for well-known keys here to provide enhanced UI. @@ -1184,6 +1229,18 @@ pub struct SessionActiveClient { /// children inside {@link SessionState.customizations}. #[serde(default, skip_serializing_if = "Option::is_none")] pub customizations: Option>, + /// Canvas declarations this client contributes as a provider. Published + /// atomically with the rest of the active-client entry via + /// `session/activeClientSet` — exactly like {@link tools} — so there is no + /// separate canvas-provider change action. The host folds these into + /// {@link SessionState.canvases} with + /// {@link SessionCanvasDeclaration.source | `source`} set to + /// `{ kind: 'client', clientId }` and routes + /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back + /// to this client. Only meaningful for a client that declared + /// {@link ClientCapabilities.canvas}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub canvas_providers: Option>, } /// A user-input elicitation surfaced at the session level, mirroring one entry @@ -3599,6 +3656,166 @@ pub struct ResourceChange { pub r#type: ResourceChangeType, } +/// One named action a canvas exposes to the agent, mirroring the shape of a +/// {@link ToolDefinition} entry. Unique within its owning +/// `(extensionId, canvasId)`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCanvasAction { + /// Action name, unique within the owning `(extensionId, canvasId)`. + pub name: String, + /// Human-readable description of what the action does. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// JSON Schema for the action's input. Opaque to AHP; mirrors the + /// {@link ToolDefinition.inputSchema} shape. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_schema: Option, +} + +/// One entry in the aggregated {@link SessionState.canvases} registry — a canvas +/// the agent can open, contributed by a server-side or client-declared provider. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCanvasDeclaration { + /// Owning provider id. Stable across declarations and instances. + pub extension_id: String, + /// Human-readable provider name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// Provider-local canvas id. Unique within `extensionId`. + pub canvas_id: String, + /// Human-readable canvas name. + pub display_name: String, + /// Human-readable description of the canvas. + pub description: String, + /// JSON Schema for the canvas's open input. Opaque to AHP; mirrors the + /// {@link ToolDefinition.inputSchema} shape. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Actions this canvas exposes to the agent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub actions: Option>, + /// Where the declaration came from — for routing and cleanup. + pub source: CanvasProviderSource, +} + +/// The lighter declaration shape a client publishes on +/// {@link SessionActiveClient.canvasProviders}. The host derives the +/// `extensionId` and {@link SessionCanvasDeclaration.source | `source`} when +/// folding it into {@link SessionState.canvases}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientCanvasDeclaration { + /// Provider-local canvas id, unique within the publishing client. + pub canvas_id: String, + /// Human-readable canvas name. + pub display_name: String, + /// Human-readable description of the canvas. + pub description: String, + /// JSON Schema for the canvas's open input. Opaque to AHP. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Actions this canvas exposes to the agent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub actions: Option>, +} + +/// A lightweight catalogue entry for one open canvas instance, surfaced on +/// {@link SessionState.openCanvases}. The authoritative, mutable per-instance +/// state lives on the instance's own {@link CanvasState} channel; this entry +/// exists so a subscriber can discover the channel URI and render it without +/// subscribing to every instance. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenCanvasRef { + /// Server-assigned instance handle, unique within the session. + pub instance_id: String, + /// The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load + /// the full {@link CanvasState}. + pub channel: Uri, + /// Provider-local canvas id this instance was opened from. + pub canvas_id: String, + /// Owning provider id. + pub extension_id: String, + /// Human-readable provider name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// Current instance title, mirrored from {@link CanvasState.title}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Whether the instance's provider is currently available. + pub availability: CanvasAvailability, +} + +/// A canvas provided by the host. Carries no `clientId` — server-side provider +/// requests are resolved host-internally rather than routed to a peer. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasServerProviderSource {} + +/// A canvas provided by a connected client. The host routes +/// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas +/// to the identified client. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasClientProviderSource { + /// `clientId` of the providing client (matches `initialize`). + pub client_id: String, +} + +/// Full state for a single open canvas instance, delivered when a client +/// subscribes to the instance's `ahp-canvas:/` channel. +/// +/// One channel exists per open instance — the same "one channel per resource" +/// convention used by terminals, changesets, and resource watches. The +/// lightweight catalogue entry that advertises this channel is +/// {@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the +/// authoritative, mutable per-instance view a renderer reads. +/// +/// Rendering is state-driven: a client renders the canvas by reading +/// {@link url} and resolving it per the renderer's URL policy — directly for a +/// reachable address, or over this channel via `canvasReadResource` for an +/// `ahp-canvas-content:` address. It never receives a "render this" request. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasState { + /// Server-assigned instance handle, unique within the session. + pub instance_id: String, + /// Provider-local canvas id this instance was opened from. + pub canvas_id: String, + /// Owning provider id. + pub extension_id: String, + /// Human-readable provider name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// Human-readable canvas name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Input the agent supplied when opening the instance. Retained so the + /// instance can be resumed or rebound after a reconnect. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Current instance title. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Provider-defined status string (opaque to AHP). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, + /// Renderer-targeted address for the opaque canvas content — either a + /// directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) + /// or a channel-served `ahp-canvas-content://` address the + /// renderer resolves over this channel with `canvasReadResource`. The + /// renderer dispatches on the scheme and enforces its URL policy. See + /// {@link /specification/canvas-channel | Canvas Channel}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Whether this instance's provider is currently available. + pub availability: CanvasAvailability, + /// Which provider owns the callbacks (`canvasOpen` / … ) for this instance. + pub provider: CanvasProviderSource, +} + // ─── Discriminated Unions ───────────────────────────────────────────── /// How a chat came into existence. @@ -3920,13 +4137,28 @@ pub enum SessionInputRequest { Unknown(serde_json::Value), } +/// Where a canvas declaration came from — used for request routing and cleanup when its provider disconnects. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum CanvasProviderSource { + #[serde(rename = "server")] + Server(CanvasServerProviderSource), + #[serde(rename = "client")] + Client(CanvasClientProviderSource), + /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. + /// Reducers treat this as a no-op. + #[serde(untagged)] + Unknown(serde_json::Value), +} + /// The state payload of a snapshot — root, session, chat, terminal, -/// changeset, resource-watch, or annotations state. +/// changeset, resource-watch, canvas, or annotations state. /// /// Deserialized by trying session first (has required `lifecycle`), then /// chat (has required `turns`), then terminal (has required `content`), /// then changeset (has required `status` and `files`), then resource-watch -/// (has required `root` and `recursive`), then annotations (has required +/// (has required `root` and `recursive`), then canvas (has required +/// `canvasId` and `provider`), then annotations (has required /// `annotations`), then root. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] @@ -3936,6 +4168,7 @@ pub enum SnapshotState { Terminal(Box), Changeset(Box), ResourceWatch(Box), + Canvas(Box), Annotations(Box), Root(Box), } diff --git a/clients/rust/crates/ahp-types/src/version.rs b/clients/rust/crates/ahp-types/src/version.rs index 5b04e5bc..8db522a4 100644 --- a/clients/rust/crates/ahp-types/src/version.rs +++ b/clients/rust/crates/ahp-types/src/version.rs @@ -5,7 +5,7 @@ #![allow(missing_docs)] /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -pub const PROTOCOL_VERSION: &str = "0.5.1"; +pub const PROTOCOL_VERSION: &str = "0.6.0"; /// Every protocol version this crate is willing to negotiate, ordered /// most-preferred-first. The first entry equals [`PROTOCOL_VERSION`]. @@ -13,4 +13,4 @@ pub const PROTOCOL_VERSION: &str = "0.5.1"; /// Consumers building `InitializeParams` should pass this slice (or a /// derived `Vec`) so the same client binary can fall back to /// older protocol versions if the host doesn't accept the newest one. -pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.5.1", "0.5.0"]; +pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.6.0", "0.5.1", "0.5.0"]; diff --git a/clients/rust/crates/ahp/src/multi_host_state_mirror.rs b/clients/rust/crates/ahp/src/multi_host_state_mirror.rs index 7288894b..fc682c8a 100644 --- a/clients/rust/crates/ahp/src/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/src/multi_host_state_mirror.rs @@ -37,13 +37,14 @@ use std::collections::HashMap; use ahp_types::actions::ActionEnvelope; use ahp_types::common::ROOT_RESOURCE_URI; use ahp_types::state::{ - AnnotationsState, ChangesetState, ChatState, ResourceWatchState, RootState, SessionState, - SnapshotState, TerminalState, + AnnotationsState, CanvasState, ChangesetState, ChatState, ResourceWatchState, RootState, + SessionState, SnapshotState, TerminalState, }; use crate::hosts::{HostId, HostSubscriptionEvent}; use crate::reducers::{ - apply_action_to_chat, apply_action_to_root, apply_action_to_session, apply_action_to_terminal, + apply_action_to_canvas, apply_action_to_chat, apply_action_to_root, apply_action_to_session, + apply_action_to_terminal, }; use crate::SubscriptionEvent; @@ -92,6 +93,7 @@ pub struct MultiHostStateMirror { changesets: HashMap, annotations: HashMap, resource_watches: HashMap, + canvases: HashMap, } impl MultiHostStateMirror { @@ -135,6 +137,11 @@ impl MultiHostStateMirror { &self.resource_watches } + /// Borrow the canvas states map keyed by `(host_id, uri)`. + pub fn canvases(&self) -> &HashMap { + &self.canvases + } + /// Convenience: apply a [`HostSubscriptionEvent`] produced by /// [`crate::hosts::MultiHostClient::events`]. Action envelopes are /// routed through the reducer; non-action events (session-summary @@ -176,6 +183,10 @@ impl MultiHostStateMirror { } if let Some(terminal) = self.terminals.get_mut(&key) { apply_action_to_terminal(terminal, &envelope.action); + return; + } + if let Some(canvas) = self.canvases.get_mut(&key) { + apply_action_to_canvas(canvas, &envelope.action); } // Changesets are seeded by `apply_snapshot` only — there's no // changeset reducer in the SDK today (matching the Swift @@ -208,6 +219,9 @@ impl MultiHostStateMirror { SnapshotState::ResourceWatch(state) => { self.resource_watches.insert(key, state.as_ref().clone()); } + SnapshotState::Canvas(state) => { + self.canvases.insert(key, state.as_ref().clone()); + } SnapshotState::Annotations(state) => { self.annotations.insert(key, state.as_ref().clone()); } @@ -215,7 +229,7 @@ impl MultiHostStateMirror { } /// Drop every slot keyed under `host` — root state, sessions, - /// terminals, changesets, resource watches, and annotations. + /// terminals, changesets, resource watches, canvases, and annotations. pub fn reset_host(&mut self, host: &HostId) { self.root_states.remove(host); self.sessions.retain(|key, _| &key.host_id != host); @@ -224,6 +238,7 @@ impl MultiHostStateMirror { self.changesets.retain(|key, _| &key.host_id != host); self.annotations.retain(|key, _| &key.host_id != host); self.resource_watches.retain(|key, _| &key.host_id != host); + self.canvases.retain(|key, _| &key.host_id != host); } /// Drop every host's state. @@ -235,5 +250,6 @@ impl MultiHostStateMirror { self.changesets.clear(); self.annotations.clear(); self.resource_watches.clear(); + self.canvases.clear(); } } diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index afacf342..688d154e 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -3,8 +3,9 @@ //! Reducers mutate state in place and return a [`ReduceOutcome`]. Use //! [`apply_action_to_root`], [`apply_action_to_session`], //! [`apply_action_to_chat`], [`apply_action_to_terminal`], -//! [`apply_action_to_changeset`], [`apply_action_to_annotations`], and -//! [`apply_action_to_resource_watch`] to dispatch any [`StateAction`] +//! [`apply_action_to_changeset`], [`apply_action_to_annotations`], +//! [`apply_action_to_resource_watch`], and [`apply_action_to_canvas`] to +//! dispatch any [`StateAction`] //! against the matching scope; unrelated actions short-circuit as //! [`ReduceOutcome::OutOfScope`] so a client holding every state tree can //! blindly fan each action out. @@ -57,7 +58,7 @@ use ahp_types::actions::{ ChatToolCallResultConfirmedAction, ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, AnnotationsState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, + ActiveTurn, AnnotationsState, CanvasState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, @@ -650,6 +651,14 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - state.server_tools = Some(a.tools.clone()); ReduceOutcome::Applied } + StateAction::SessionCanvasesChanged(a) => { + state.canvases = Some(a.canvases.clone()); + ReduceOutcome::Applied + } + StateAction::SessionOpenCanvasesChanged(a) => { + state.open_canvases = Some(a.open_canvases.clone()); + ReduceOutcome::Applied + } StateAction::SessionActiveClientSet(a) => { if let Some(idx) = state .active_clients @@ -1604,6 +1613,39 @@ pub fn apply_action_to_resource_watch( } } +// ─── Canvas Reducer ─────────────────────────────────────────────── + +/// Apply a [`StateAction`] to a [`CanvasState`] in place. +/// +/// `canvas/updated` is a sparse merge — each present field overwrites the +/// corresponding [`CanvasState`] field and an absent field preserves the +/// current value. `canvas/closeRequested` and `canvas/message` are pure +/// signals with no state effect (the host acts on them out of band), +/// mirroring how `terminal/input` is side-effect-only. Actions targeting a +/// different scope short-circuit as [`ReduceOutcome::OutOfScope`]. +pub fn apply_action_to_canvas(state: &mut CanvasState, action: &StateAction) -> ReduceOutcome { + match action { + StateAction::CanvasUpdated(a) => { + if let Some(title) = &a.title { + state.title = Some(title.clone()); + } + if let Some(status) = &a.status { + state.status = Some(status.clone()); + } + if let Some(url) = &a.url { + state.url = Some(url.clone()); + } + if let Some(availability) = &a.availability { + state.availability = availability.clone(); + } + ReduceOutcome::Applied + } + StateAction::CanvasCloseRequested(_) => ReduceOutcome::NoOp, + StateAction::CanvasMessage(_) => ReduceOutcome::NoOp, + _ => ReduceOutcome::OutOfScope, + } +} + #[cfg(test)] mod tests { use super::*; @@ -1636,6 +1678,8 @@ mod tests { lifecycle: SessionLifecycle::Creating, creation_error: None, server_tools: None, + canvases: None, + open_canvases: None, active_clients: Vec::new(), chats: Vec::new(), default_chat: None, @@ -2048,6 +2092,14 @@ mod tests { &file_name, description, ), + "canvas" => run_fixture::( + initial, + expected, + &parsed_actions, + apply_action_to_canvas, + &file_name, + description, + ), other => { panic!("{file_name}: unknown reducer type '{other}'"); } diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 5f8ad2df..cbb1ce89 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -56,6 +56,8 @@ fn session_state(title: &str, _resource: &str) -> SessionState { lifecycle: SessionLifecycle::Ready, creation_error: None, server_tools: None, + canvases: None, + open_canvases: None, active_clients: vec![], chats: vec![], default_chat: None, diff --git a/clients/rust/release-metadata.json b/clients/rust/release-metadata.json index 1cdb02a6..dca82575 100644 --- a/clients/rust/release-metadata.json +++ b/clients/rust/release-metadata.json @@ -2,6 +2,7 @@ "client": "rust", "packageVersion": "0.5.0", "supportedProtocolVersions": [ + "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 95630c99..fba268de 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -81,6 +81,11 @@ public enum ActionType: String, Codable, Sendable { case terminalCommandExecuted = "terminal/commandExecuted" case terminalCommandFinished = "terminal/commandFinished" case resourceWatchChanged = "resourceWatch/changed" + case sessionCanvasesChanged = "session/canvasesChanged" + case sessionOpenCanvasesChanged = "session/openCanvasesChanged" + case canvasUpdated = "canvas/updated" + case canvasCloseRequested = "canvas/closeRequested" + case canvasMessage = "canvas/message" } // MARK: - Action Infrastructure @@ -971,6 +976,34 @@ public struct SessionServerToolsChangedAction: Codable, Sendable { } } +public struct SessionCanvasesChangedAction: Codable, Sendable { + public var type: ActionType + /// Updated canvas registry (full replacement). + public var canvases: [SessionCanvasDeclaration] + + public init( + type: ActionType, + canvases: [SessionCanvasDeclaration] + ) { + self.type = type + self.canvases = canvases + } +} + +public struct SessionOpenCanvasesChangedAction: Codable, Sendable { + public var type: ActionType + /// Updated open-instance catalogue (full replacement). + public var openCanvases: [OpenCanvasRef] + + public init( + type: ActionType, + openCanvases: [OpenCanvasRef] + ) { + self.type = type + self.openCanvases = openCanvases + } +} + public struct SessionActiveClientSetAction: Codable, Sendable { public var type: ActionType /// The active client to add or update, matched by `clientId`. @@ -1718,6 +1751,56 @@ public struct ResourceWatchChangedAction: Codable, Sendable { } } +public struct CanvasUpdatedAction: Codable, Sendable { + public var type: ActionType + /// New title. Absent preserves the current title. + public var title: String? + /// New provider-defined status. Absent preserves the current status. + public var status: String? + /// New content address. Absent preserves the current url. + public var url: String? + /// New availability. Absent preserves the current availability. + public var availability: CanvasAvailability? + + public init( + type: ActionType, + title: String? = nil, + status: String? = nil, + url: String? = nil, + availability: CanvasAvailability? = nil + ) { + self.type = type + self.title = title + self.status = status + self.url = url + self.availability = availability + } +} + +public struct CanvasCloseRequestedAction: Codable, Sendable { + public var type: ActionType + + public init( + type: ActionType + ) { + self.type = type + } +} + +public struct CanvasMessageAction: Codable, Sendable { + public var type: ActionType + /// Opaque, provider-defined message payload. + public var payload: AnyCodable + + public init( + type: ActionType, + payload: AnyCodable + ) { + self.type = type + self.payload = payload + } +} + // MARK: - Partial Summary Types public struct PartialChatSummary: Codable, Sendable { @@ -1801,6 +1884,8 @@ public enum StateAction: Codable, Sendable { case sessionActivityChanged(SessionActivityChangedAction) case sessionChangesetsChanged(SessionChangesetsChangedAction) case sessionServerToolsChanged(SessionServerToolsChangedAction) + case sessionCanvasesChanged(SessionCanvasesChangedAction) + case sessionOpenCanvasesChanged(SessionOpenCanvasesChangedAction) case sessionActiveClientSet(SessionActiveClientSetAction) case sessionActiveClientRemoved(SessionActiveClientRemovedAction) case sessionInputNeededSet(SessionInputNeededSetAction) @@ -1846,6 +1931,9 @@ public enum StateAction: Codable, Sendable { case terminalCommandExecuted(TerminalCommandExecutedAction) case terminalCommandFinished(TerminalCommandFinishedAction) case resourceWatchChanged(ResourceWatchChangedAction) + case canvasUpdated(CanvasUpdatedAction) + case canvasCloseRequested(CanvasCloseRequestedAction) + case canvasMessage(CanvasMessageAction) /// Unknown or future action type; reducers treat this as a no-op. /// The raw payload (including its `type` discriminant) is preserved /// as an `AnyCodable` so a decode→encode round-trip re-emits it @@ -1918,6 +2006,10 @@ public enum StateAction: Codable, Sendable { self = .sessionChangesetsChanged(try SessionChangesetsChangedAction(from: decoder)) case "session/serverToolsChanged": self = .sessionServerToolsChanged(try SessionServerToolsChangedAction(from: decoder)) + case "session/canvasesChanged": + self = .sessionCanvasesChanged(try SessionCanvasesChangedAction(from: decoder)) + case "session/openCanvasesChanged": + self = .sessionOpenCanvasesChanged(try SessionOpenCanvasesChangedAction(from: decoder)) case "session/activeClientSet": self = .sessionActiveClientSet(try SessionActiveClientSetAction(from: decoder)) case "session/activeClientRemoved": @@ -2008,6 +2100,12 @@ public enum StateAction: Codable, Sendable { self = .terminalCommandFinished(try TerminalCommandFinishedAction(from: decoder)) case "resourceWatch/changed": self = .resourceWatchChanged(try ResourceWatchChangedAction(from: decoder)) + case "canvas/updated": + self = .canvasUpdated(try CanvasUpdatedAction(from: decoder)) + case "canvas/closeRequested": + self = .canvasCloseRequested(try CanvasCloseRequestedAction(from: decoder)) + case "canvas/message": + self = .canvasMessage(try CanvasMessageAction(from: decoder)) default: self = .unknown(try AnyCodable(from: decoder)) } @@ -2045,6 +2143,8 @@ public enum StateAction: Codable, Sendable { case .sessionActivityChanged(let v): try v.encode(to: encoder) case .sessionChangesetsChanged(let v): try v.encode(to: encoder) case .sessionServerToolsChanged(let v): try v.encode(to: encoder) + case .sessionCanvasesChanged(let v): try v.encode(to: encoder) + case .sessionOpenCanvasesChanged(let v): try v.encode(to: encoder) case .sessionActiveClientSet(let v): try v.encode(to: encoder) case .sessionActiveClientRemoved(let v): try v.encode(to: encoder) case .sessionInputNeededSet(let v): try v.encode(to: encoder) @@ -2090,6 +2190,9 @@ public enum StateAction: Codable, Sendable { case .terminalCommandExecuted(let v): try v.encode(to: encoder) case .terminalCommandFinished(let v): try v.encode(to: encoder) case .resourceWatchChanged(let v): try v.encode(to: encoder) + case .canvasUpdated(let v): try v.encode(to: encoder) + case .canvasCloseRequested(let v): try v.encode(to: encoder) + case .canvasMessage(let v): try v.encode(to: encoder) case .unknown(let value): try value.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 6113d421..1ec36d4e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -154,11 +154,23 @@ public struct ClientCapabilities: Codable, Sendable { /// capability is declared. Clients that omit it MUST treat /// App-bearing tool calls as ordinary MCP tool calls. public var mcpApps: [String: AnyCodable]? + /// Client can render canvases and host client-declared canvas providers — it + /// can render an opaque canvas URL in an isolated surface, and it can answer + /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + /// it declares via {@link SessionActiveClient.canvasProviders}. + /// + /// Hosts SHOULD only populate {@link SessionState.canvases} / + /// {@link SessionState.openCanvases} and only route canvas requests to a + /// client that declared this capability. Clients that omit it see no canvas + /// surface. See {@link /specification/canvas-channel | Canvas Channel}. + public var canvas: [String: AnyCodable]? public init( - mcpApps: [String: AnyCodable]? = nil + mcpApps: [String: AnyCodable]? = nil, + canvas: [String: AnyCodable]? = nil ) { self.mcpApps = mcpApps + self.canvas = canvas } } @@ -1283,6 +1295,166 @@ public struct ChangesetOperationFollowUp: Codable, Sendable { } } +public struct CanvasOpenParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Provider-local canvas id to open. + public var canvasId: String + /// Owning provider id. + public var extensionId: String + /// Caller-minted handle for the new instance. + public var instanceId: String + /// Open input, validated by the provider against its declared schema. + public var input: [String: AnyCodable]? + + public init( + channel: String, + canvasId: String, + extensionId: String, + instanceId: String, + input: [String: AnyCodable]? = nil + ) { + self.channel = channel + self.canvasId = canvasId + self.extensionId = extensionId + self.instanceId = instanceId + self.input = input + } +} + +public struct CanvasOpenResult: Codable, Sendable { + /// Initial content address for the instance (see {@link CanvasState.url}). + public var url: String? + /// Initial title. + public var title: String? + /// Initial provider-defined status. + public var status: String? + + public init( + url: String? = nil, + title: String? = nil, + status: String? = nil + ) { + self.url = url + self.title = title + self.status = status + } +} + +public struct CanvasInvokeActionParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Instance handle the action targets. + public var instanceId: String + /// Provider-local canvas id of the instance. + public var canvasId: String + /// Owning provider id. + public var extensionId: String + /// Declared action name to invoke. + public var actionName: String + /// Action input, validated by the provider against its declared schema. + public var input: [String: AnyCodable]? + + public init( + channel: String, + instanceId: String, + canvasId: String, + extensionId: String, + actionName: String, + input: [String: AnyCodable]? = nil + ) { + self.channel = channel + self.instanceId = instanceId + self.canvasId = canvasId + self.extensionId = extensionId + self.actionName = actionName + self.input = input + } +} + +public struct CanvasInvokeActionResult: Codable, Sendable { + /// Opaque, provider-defined return value. + public var value: AnyCodable? + + public init( + value: AnyCodable? = nil + ) { + self.value = value + } +} + +public struct CanvasCloseParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Instance handle to close. + public var instanceId: String + /// Provider-local canvas id of the instance. + public var canvasId: String + /// Owning provider id. + public var extensionId: String + + public init( + channel: String, + instanceId: String, + canvasId: String, + extensionId: String + ) { + self.channel = channel + self.instanceId = instanceId + self.canvasId = canvasId + self.extensionId = extensionId + } +} + +public struct CanvasReadResourceParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// An `ahp-canvas-content://` content URI to read. + public var uri: String + + public init( + channel: String, + uri: String + ) { + self.channel = channel + self.uri = uri + } +} + +public struct CanvasReadResourceResult: Codable, Sendable { + /// The resolved content parts, wrapped for forward compatibility. + public var contents: [CanvasResourceContent] + + public init( + contents: [CanvasResourceContent] + ) { + self.contents = contents + } +} + +public struct CanvasResourceContent: Codable, Sendable { + /// The content URI this part resolves. + public var uri: String + /// MIME type of the content, when known. + public var mimeType: String? + /// UTF-8 text content, for text payloads. + public var text: String? + /// Base64-encoded content, for binary payloads. + public var blob: String? + + public init( + uri: String, + mimeType: String? = nil, + text: String? = nil, + blob: String? = nil + ) { + self.uri = uri + self.mimeType = mimeType + self.text = text + self.blob = blob + } +} + // MARK: - ReconnectResult Union public enum ReconnectResult: Codable, Sendable { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift index 3b220131..7ee5cc69 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift @@ -40,6 +40,10 @@ public enum AhpErrorCodes { public static let permissionDenied = -32009 /// The target resource already exists and the operation does not allow overwriting public static let alreadyExists = -32010 + /// An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state + public static let conflict = -32011 + /// A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` + public static let canvasProviderError = -32012 } // MARK: - Error Detail Payloads @@ -82,3 +86,18 @@ public struct UnsupportedProtocolVersionErrorData: Codable, Sendable { self.supportedVersions = supportedVersions } } + +public struct CanvasProviderErrorData: Codable, Sendable { + /// Provider-defined error code identifying the failure. + public var code: String + /// Human-readable error message. + public var message: String + + public init( + code: String, + message: String + ) { + self.code = code + self.message = message + } +} diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 4e278a52..331fa648 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -378,6 +378,24 @@ public enum ResourceChangeType: String, Codable, Sendable { case deleted = "deleted" } +/// Availability of a canvas provider or open instance. +public enum CanvasAvailability: String, Codable, Sendable { + /// The provider is connected and can service requests for this canvas. + case ready = "ready" + /// The provider is temporarily unavailable (for example, a client provider + /// that disconnected). The entry is retained so it can be restored when the + /// provider reconnects; in-flight requests fail until then. + case stale = "stale" +} + +/// Discriminant for {@link CanvasProviderSource}. +public enum CanvasProviderKind: String, Codable, Sendable { + /// The canvas is provided by the host itself. + case server = "server" + /// The canvas is provided by a connected client. + case client = "client" +} + // MARK: - State Types public struct Icon: Codable, Sendable { @@ -1070,6 +1088,25 @@ public struct SessionState: Codable, Sendable { /// chats raise requests and removes them with `session/inputNeededRemoved` /// once the underlying request resolves. public var inputNeeded: [SessionInputRequest]? + /// Aggregated canvas registry currently exposed to the agent — the union of + /// every connected provider (server-side and client-declared). Each entry + /// describes a canvas the agent can open; the host folds + /// {@link SessionActiveClient.canvasProviders | client-declared providers} + /// into this list alongside its own server-side providers. + /// + /// Full-replacement via `session/canvasesChanged`. Populated only when at + /// least one connected client declared {@link ClientCapabilities.canvas}; + /// absent for sessions with no canvas surface. See + /// {@link /specification/canvas-channel | Canvas Channel}. + public var canvases: [SessionCanvasDeclaration]? + /// Lightweight catalogue of currently-open canvas instances. Each entry + /// carries the instance's `ahp-canvas:/` channel URI so a subscriber can + /// subscribe to the full {@link CanvasState} and render it — analogous to how + /// {@link RootState.terminals} catalogues live terminals whose full state + /// lives on each terminal channel. + /// + /// Full-replacement via `session/openCanvasesChanged`. + public var openCanvases: [OpenCanvasRef]? /// Additional provider-specific metadata for this session. /// /// Clients MAY look for well-known keys here to provide enhanced UI. @@ -1095,6 +1132,8 @@ public struct SessionState: Codable, Sendable { case customizations case changesets case inputNeeded + case canvases + case openCanvases case meta = "_meta" } @@ -1116,6 +1155,8 @@ public struct SessionState: Codable, Sendable { customizations: [Customization]? = nil, changesets: [Changeset]? = nil, inputNeeded: [SessionInputRequest]? = nil, + canvases: [SessionCanvasDeclaration]? = nil, + openCanvases: [OpenCanvasRef]? = nil, meta: [String: AnyCodable]? = nil ) { self.provider = provider @@ -1135,6 +1176,8 @@ public struct SessionState: Codable, Sendable { self.customizations = customizations self.changesets = changesets self.inputNeeded = inputNeeded + self.canvases = canvases + self.openCanvases = openCanvases self.meta = meta } } @@ -1153,17 +1196,30 @@ public struct SessionActiveClient: Codable, Sendable { /// plugins in memory and rely on the host to expand them into concrete /// children inside {@link SessionState.customizations}. public var customizations: [ClientPluginCustomization]? + /// Canvas declarations this client contributes as a provider. Published + /// atomically with the rest of the active-client entry via + /// `session/activeClientSet` — exactly like {@link tools} — so there is no + /// separate canvas-provider change action. The host folds these into + /// {@link SessionState.canvases} with + /// {@link SessionCanvasDeclaration.source | `source`} set to + /// `{ kind: 'client', clientId }` and routes + /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back + /// to this client. Only meaningful for a client that declared + /// {@link ClientCapabilities.canvas}. + public var canvasProviders: [ClientCanvasDeclaration]? public init( clientId: String, displayName: String? = nil, tools: [ToolDefinition], - customizations: [ClientPluginCustomization]? = nil + customizations: [ClientPluginCustomization]? = nil, + canvasProviders: [ClientCanvasDeclaration]? = nil ) { self.clientId = clientId self.displayName = displayName self.tools = tools self.customizations = customizations + self.canvasProviders = canvasProviders } } @@ -4396,6 +4452,210 @@ public struct ResourceChange: Codable, Sendable { } } +public struct SessionCanvasAction: Codable, Sendable { + /// Action name, unique within the owning `(extensionId, canvasId)`. + public var name: String + /// Human-readable description of what the action does. + public var description: String? + /// JSON Schema for the action's input. Opaque to AHP; mirrors the + /// {@link ToolDefinition.inputSchema} shape. + public var inputSchema: [String: AnyCodable]? + + public init( + name: String, + description: String? = nil, + inputSchema: [String: AnyCodable]? = nil + ) { + self.name = name + self.description = description + self.inputSchema = inputSchema + } +} + +public struct SessionCanvasDeclaration: Codable, Sendable { + /// Owning provider id. Stable across declarations and instances. + public var extensionId: String + /// Human-readable provider name. + public var extensionName: String? + /// Provider-local canvas id. Unique within `extensionId`. + public var canvasId: String + /// Human-readable canvas name. + public var displayName: String + /// Human-readable description of the canvas. + public var description: String + /// JSON Schema for the canvas's open input. Opaque to AHP; mirrors the + /// {@link ToolDefinition.inputSchema} shape. + public var inputSchema: [String: AnyCodable]? + /// Actions this canvas exposes to the agent. + public var actions: [SessionCanvasAction]? + /// Where the declaration came from — for routing and cleanup. + public var source: CanvasProviderSource + + public init( + extensionId: String, + extensionName: String? = nil, + canvasId: String, + displayName: String, + description: String, + inputSchema: [String: AnyCodable]? = nil, + actions: [SessionCanvasAction]? = nil, + source: CanvasProviderSource + ) { + self.extensionId = extensionId + self.extensionName = extensionName + self.canvasId = canvasId + self.displayName = displayName + self.description = description + self.inputSchema = inputSchema + self.actions = actions + self.source = source + } +} + +public struct ClientCanvasDeclaration: Codable, Sendable { + /// Provider-local canvas id, unique within the publishing client. + public var canvasId: String + /// Human-readable canvas name. + public var displayName: String + /// Human-readable description of the canvas. + public var description: String + /// JSON Schema for the canvas's open input. Opaque to AHP. + public var inputSchema: [String: AnyCodable]? + /// Actions this canvas exposes to the agent. + public var actions: [SessionCanvasAction]? + + public init( + canvasId: String, + displayName: String, + description: String, + inputSchema: [String: AnyCodable]? = nil, + actions: [SessionCanvasAction]? = nil + ) { + self.canvasId = canvasId + self.displayName = displayName + self.description = description + self.inputSchema = inputSchema + self.actions = actions + } +} + +public struct OpenCanvasRef: Codable, Sendable { + /// Server-assigned instance handle, unique within the session. + public var instanceId: String + /// The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load + /// the full {@link CanvasState}. + public var channel: String + /// Provider-local canvas id this instance was opened from. + public var canvasId: String + /// Owning provider id. + public var extensionId: String + /// Human-readable provider name. + public var extensionName: String? + /// Current instance title, mirrored from {@link CanvasState.title}. + public var title: String? + /// Whether the instance's provider is currently available. + public var availability: CanvasAvailability + + public init( + instanceId: String, + channel: String, + canvasId: String, + extensionId: String, + extensionName: String? = nil, + title: String? = nil, + availability: CanvasAvailability + ) { + self.instanceId = instanceId + self.channel = channel + self.canvasId = canvasId + self.extensionId = extensionId + self.extensionName = extensionName + self.title = title + self.availability = availability + } +} + +public struct CanvasServerProviderSource: Codable, Sendable { + public var kind: CanvasProviderKind + + public init( + kind: CanvasProviderKind + ) { + self.kind = kind + } +} + +public struct CanvasClientProviderSource: Codable, Sendable { + public var kind: CanvasProviderKind + /// `clientId` of the providing client (matches `initialize`). + public var clientId: String + + public init( + kind: CanvasProviderKind, + clientId: String + ) { + self.kind = kind + self.clientId = clientId + } +} + +public struct CanvasState: Codable, Sendable { + /// Server-assigned instance handle, unique within the session. + public var instanceId: String + /// Provider-local canvas id this instance was opened from. + public var canvasId: String + /// Owning provider id. + public var extensionId: String + /// Human-readable provider name. + public var extensionName: String? + /// Human-readable canvas name. + public var displayName: String? + /// Input the agent supplied when opening the instance. Retained so the + /// instance can be resumed or rebound after a reconnect. + public var input: [String: AnyCodable]? + /// Current instance title. + public var title: String? + /// Provider-defined status string (opaque to AHP). + public var status: String? + /// Renderer-targeted address for the opaque canvas content — either a + /// directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) + /// or a channel-served `ahp-canvas-content://` address the + /// renderer resolves over this channel with `canvasReadResource`. The + /// renderer dispatches on the scheme and enforces its URL policy. See + /// {@link /specification/canvas-channel | Canvas Channel}. + public var url: String? + /// Whether this instance's provider is currently available. + public var availability: CanvasAvailability + /// Which provider owns the callbacks (`canvasOpen` / … ) for this instance. + public var provider: CanvasProviderSource + + public init( + instanceId: String, + canvasId: String, + extensionId: String, + extensionName: String? = nil, + displayName: String? = nil, + input: [String: AnyCodable]? = nil, + title: String? = nil, + status: String? = nil, + url: String? = nil, + availability: CanvasAvailability, + provider: CanvasProviderSource + ) { + self.instanceId = instanceId + self.canvasId = canvasId + self.extensionId = extensionId + self.extensionName = extensionName + self.displayName = displayName + self.input = input + self.title = title + self.status = status + self.url = url + self.availability = availability + self.provider = provider + } +} + // MARK: - Discriminated Unions public struct ChatOriginUser: Codable, Sendable { @@ -5057,6 +5317,39 @@ public enum SessionInputRequest: Codable, Sendable { } } +public enum CanvasProviderSource: Codable, Sendable { + case server(CanvasServerProviderSource) + case client(CanvasClientProviderSource) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "server": + self = .server(try CanvasServerProviderSource(from: decoder)) + case "client": + self = .client(try CanvasClientProviderSource(from: decoder)) + default: + self = .unknown(try AnyCodable(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .server(let value): try value.encode(to: encoder) + case .client(let value): try value.encode(to: encoder) + case .unknown(let value): try value.encode(to: encoder) + } + } +} + public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) case embeddedResource(ToolResultEmbeddedResourceContent) @@ -5112,7 +5405,7 @@ public enum ToolResultContent: Codable, Sendable { } } -/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, or annotations state. +/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, canvas, or annotations state. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) @@ -5120,6 +5413,7 @@ public enum SnapshotState: Codable, Sendable { case terminal(TerminalState) case changeset(ChangesetState) case resourceWatch(ResourceWatchState) + case canvas(CanvasState) case annotations(AnnotationsState) public init(from decoder: Decoder) throws { @@ -5137,6 +5431,8 @@ public enum SnapshotState: Codable, Sendable { self = .changeset(changeset) } else if let resourceWatch = try? ResourceWatchState(from: decoder) { self = .resourceWatch(resourceWatch) + } else if let canvas = try? CanvasState(from: decoder) { + self = .canvas(canvas) } else if let annotations = try? AnnotationsState(from: decoder) { self = .annotations(annotations) } else { @@ -5152,6 +5448,7 @@ public enum SnapshotState: Codable, Sendable { case .terminal(let state): try state.encode(to: encoder) case .changeset(let state): try state.encode(to: encoder) case .resourceWatch(let state): try state.encode(to: encoder) + case .canvas(let state): try state.encode(to: encoder) case .annotations(let state): try state.encode(to: encoder) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift index d4690313..b170a858 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift @@ -3,7 +3,7 @@ import Foundation /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -public let PROTOCOL_VERSION: String = "0.5.1" +public let PROTOCOL_VERSION: String = "0.6.0" /// Every protocol version this package is willing to negotiate, /// ordered most-preferred-first. The first entry equals @@ -13,6 +13,7 @@ public let PROTOCOL_VERSION: String = "0.5.1" /// `InitializeParams` so the same client binary can fall back to older /// protocol versions if the host doesn't accept the newest one. public let SUPPORTED_PROTOCOL_VERSIONS: [String] = [ + "0.6.0", "0.5.1", "0.5.0", ] diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 0ed159e7..de5aca87 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -599,6 +599,16 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.serverTools = a.tools return next + case .sessionCanvasesChanged(let a): + var next = state + next.canvases = a.canvases + return next + + case .sessionOpenCanvasesChanged(let a): + var next = state + next.openCanvases = a.openCanvases + return next + case .sessionActiveClientSet(let a): var next = state if let idx = next.activeClients.firstIndex(where: { $0.clientId == a.activeClient.clientId }) { @@ -1213,3 +1223,36 @@ public func resourceWatchReducer(state: ResourceWatchState, action: StateAction) return state } } + +// MARK: - Canvas Reducer + +/// Pure reducer for canvas state. Handles every canvas action. +/// +/// `canvas/updated` is a sparse merge — a presented field (title, status, url, +/// availability) overwrites the corresponding `CanvasState` field while an +/// absent field preserves the current value. `canvas/closeRequested` and +/// `canvas/message` are pure client→host signals the host acts on out of band, +/// mirroring how `terminal/input` is side-effect-only, so they leave the state +/// unchanged. Unknown action types degrade gracefully so a client speaking an +/// older protocol stays correct if the server adds new `canvas/*` actions in a +/// future version. +public func canvasReducer(state: CanvasState, action: StateAction) -> CanvasState { + switch action { + case .canvasUpdated(let a): + var next = state + if let title = a.title { next.title = title } + if let status = a.status { next.status = status } + if let url = a.url { next.url = url } + if let availability = a.availability { next.availability = availability } + return next + + case .canvasCloseRequested: + return state + + case .canvasMessage: + return state + + default: + return state + } +} diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift index 7c44fb33..f8729f8e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift @@ -18,6 +18,7 @@ public actor AHPStateMirror { public private(set) var changesets: [String: ChangesetState] = [:] public private(set) var annotations: [String: AnnotationsState] = [:] public private(set) var resourceWatches: [String: ResourceWatchState] = [:] + public private(set) var canvases: [String: CanvasState] = [:] public init() {} @@ -49,6 +50,11 @@ public actor AHPStateMirror { terminals[channel] = terminal return } + if channel.hasPrefix("ahp-canvas:"), var canvas = canvases[channel] { + canvas = canvasReducer(state: canvas, action: action) + canvases[channel] = canvas + return + } if changesets[channel] != nil { // Changesets are also seeded by `applySnapshot` and currently // mutated only when fresh snapshots arrive. @@ -84,6 +90,8 @@ public actor AHPStateMirror { changesets[snapshot.resource] = state case .resourceWatch(let state): resourceWatches[snapshot.resource] = state + case .canvas(let state): + canvases[snapshot.resource] = state case .annotations(let state): annotations[snapshot.resource] = state } @@ -98,5 +106,6 @@ public actor AHPStateMirror { changesets.removeAll() annotations.removeAll() resourceWatches.removeAll() + canvases.removeAll() } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift index 767b7ef2..bfc5ae7a 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift @@ -50,6 +50,7 @@ public actor MultiHostStateMirror { public private(set) var changesets: [HostedResourceKey: ChangesetState] = [:] public private(set) var annotations: [HostedResourceKey: AnnotationsState] = [:] public private(set) var resourceWatches: [HostedResourceKey: ResourceWatchState] = [:] + public private(set) var canvases: [HostedResourceKey: CanvasState] = [:] public init() {} @@ -90,6 +91,11 @@ public actor MultiHostStateMirror { terminals[key] = terminal return } + if channel.hasPrefix("ahp-canvas:"), var canvas = canvases[key] { + canvas = canvasReducer(state: canvas, action: action) + canvases[key] = canvas + return + } if changesets[key] != nil { // Changesets are also seeded by `applySnapshot` and currently // mutated only when fresh snapshots arrive. @@ -128,6 +134,8 @@ public actor MultiHostStateMirror { changesets[key] = state case .resourceWatch(let state): resourceWatches[key] = state + case .canvas(let state): + canvases[key] = state case .annotations(let state): annotations[key] = state } @@ -145,6 +153,7 @@ public actor MultiHostStateMirror { changesets = changesets.filter { $0.key.hostId != host } annotations = annotations.filter { $0.key.hostId != host } resourceWatches = resourceWatches.filter { $0.key.hostId != host } + canvases = canvases.filter { $0.key.hostId != host } } /// Reset every host's state. @@ -156,5 +165,6 @@ public actor MultiHostStateMirror { changesets.removeAll() annotations.removeAll() resourceWatches.removeAll() + canvases.removeAll() } } diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift index 4eecdaab..5fd3088d 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift @@ -214,6 +214,10 @@ final class FixtureDrivenReducerTests: XCTestCase { try compareFixture(file: file, fixture: fixture, stateType: AnnotationsState.self) { state in actions.reduce(state) { annotationsReducer(state: $0, action: $1) } } + case "canvas": + try compareFixture(file: file, fixture: fixture, stateType: CanvasState.self) { state in + actions.reduce(state) { canvasReducer(state: $0, action: $1) } + } default: throw FixtureError.unsupportedReducer(fixture.reducer) } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index 7bf7e226..3be9baa9 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -42,6 +42,16 @@ the tag matches the version pinned in [`VERSION`](VERSION). lifecycle state. - Optional `model` and `tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +- Canvas channel support: the per-instance `CanvasState` plus the + `StateAction.canvasUpdated` / `StateAction.canvasCloseRequested` / + `StateAction.canvasMessage` actions, the `StateAction.sessionCanvasesChanged` + / `StateAction.sessionOpenCanvasesChanged` session actions, and the canvas + discovery types (`SessionCanvasDeclaration`, `ClientCanvasDeclaration`, + `OpenCanvasRef`, `CanvasProviderSource`) on `SessionState.canvases` / + `SessionState.openCanvases`. Adds the `ClientCapabilities.canvas` capability, + the `canvasOpen` / `canvasInvokeAction` / `canvasClose` / `canvasReadResource` + methods, and the `CanvasProviderError` error. The session reducer replaces the + canvas registry/catalogue and the canvas reducer sparse-merges `canvas/updated`. ### Removed diff --git a/clients/swift/release-metadata.json b/clients/swift/release-metadata.json index 3765f19c..328bb9fc 100644 --- a/clients/swift/release-metadata.json +++ b/clients/swift/release-metadata.json @@ -2,6 +2,7 @@ "client": "swift", "packageVersion": "0.5.0", "supportedProtocolVersions": [ + "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 88476600..407c0f15 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -46,6 +46,18 @@ hotfix escape hatch. lifecycle state. - Optional `model` and `tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +- Canvas channel support: the per-instance `CanvasState` plus the + `CanvasUpdatedAction` (`canvas/updated`), `CanvasCloseRequestedAction` + (`canvas/closeRequested`), and `CanvasMessageAction` (`canvas/message`) + actions, the `SessionCanvasesChangedAction` (`session/canvasesChanged`) and + `SessionOpenCanvasesChangedAction` (`session/openCanvasesChanged`) session + actions, and the canvas discovery types (`SessionCanvasDeclaration`, + `ClientCanvasDeclaration`, `OpenCanvasRef`, `CanvasProviderSource`) on + `SessionState.canvases` / `SessionState.openCanvases`. Adds the + `ClientCapabilities.canvas` capability, the `canvasOpen` / `canvasInvokeAction` + / `canvasClose` / `canvasReadResource` methods, and the `CanvasProviderError` + error. The session reducer replaces the canvas registry/catalogue and the + `canvasReducer` sparse-merges `canvas/updated`. ### Removed diff --git a/clients/typescript/release-metadata.json b/clients/typescript/release-metadata.json index fd3c324d..8090197b 100644 --- a/clients/typescript/release-metadata.json +++ b/clients/typescript/release-metadata.json @@ -2,6 +2,7 @@ "client": "typescript", "packageVersion": "0.5.0", "supportedProtocolVersions": [ + "0.6.0", "0.5.1", "0.5.0" ] diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 9eda4425..f90119b0 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -33,6 +33,7 @@ export default withMermaid(defineConfig({ { text: 'Actions', link: '/guide/actions' }, { text: 'Elicitation', link: '/guide/elicitation' }, { text: 'Terminals', link: '/guide/terminals' }, + { text: 'Canvases', link: '/guide/canvases' }, { text: 'Customizations', link: '/guide/customizations' }, { text: 'MCP Servers', link: '/guide/mcp' }, { text: 'Write-Ahead Reconciliation', link: '/guide/reconciliation' }, @@ -68,6 +69,7 @@ export default withMermaid(defineConfig({ { text: 'Chat Channel', link: '/specification/chat-channel' }, { text: 'Terminal Channel', link: '/specification/terminal-channel' }, { text: 'Resource Watch Channel', link: '/specification/resource-watch-channel' }, + { text: 'Canvas Channel', link: '/specification/canvas-channel' }, { text: 'Telemetry Channel', link: '/specification/telemetry-channel' }, ], }, diff --git a/docs/guide/canvases.md b/docs/guide/canvases.md new file mode 100644 index 00000000..d4b009c6 --- /dev/null +++ b/docs/guide/canvases.md @@ -0,0 +1,51 @@ +# Canvases + +A **canvas** is a rich, interactive UI surface the agent can open next to a session — a document or spreadsheet editor, a diff view, a live web preview, a design surface. Where a chat turn is a stream of messages and tool calls, a canvas is a durable, stateful *view* the user works in directly while the agent keeps assisting. + +Canvases are AHP's native counterpart to [MCP Apps](https://github.com/modelcontextprotocol/ext-apps): where an MCP App is a View embedded in a tool result, a canvas is a first-class, individually subscribable resource with its own channel, discovery surface, and lifecycle — the same "one channel per resource" model used by [terminals](/guide/terminals) and [changesets](/guide/changesets). + +## Two roles a client plays + +A client that declares the `canvas` capability can act in either or both of two roles: + +- **Renderer** — it hosts the isolated surface and displays a canvas by loading its content address. Every capable client is a renderer. +- **Provider** — it *contributes* canvases (a VS Code extension exposing a "diff" canvas, say) and answers the open/invoke/close callbacks for them. A canvas can equally be provided by the host itself. + +The provider owns behaviour; the renderer owns presentation. They are frequently different clients — one client provides a canvas, another renders it — which is exactly why canvas state is synchronised through AHP rather than kept in one process. + +## Discovery + +The session advertises two things once any client declares the capability: + +- **A registry** (`SessionState.canvases`) of every canvas the agent *can* open. The host aggregates it from server-side declarations and the providers each client publishes on `SessionActiveClient.canvasProviders`. +- **An open catalogue** (`SessionState.openCanvases`) of instances that *are* open. Each entry carries the instance's `ahp-canvas:/` channel URI, so a client can render an open canvas by subscribing to that one channel — it never has to subscribe to every instance to know what exists. + +Both lists are published with full-replacement actions (`session/canvasesChanged`, `session/openCanvasesChanged`), matching how the tools catalogue is republished atomically. + +## State-driven rendering + +The defining idea: **a renderer renders from state, not from requests.** Subscribing to a canvas instance yields a `CanvasState` with a `url`, and the renderer resolves that address: + +- a directly-loadable `https:`, in-process, or `http://localhost` address is loaded straight into the surface; or +- an `ahp-canvas-content://` address is resolved over the instance's own channel with `canvasReadResource` — the escape hatch for relayed deployments where the renderer can't reach the host's network directly. + +When the canvas changes, the provider emits a `canvas/updated` action that sparse-merges the new `title` / `status` / `url` / `availability` into state, and every subscriber re-renders. There is no imperative "draw this" message anywhere in the protocol. + +## Interaction + +Two paths carry interaction back to the provider: + +- **Declared actions.** A canvas can declare named actions (with input schemas) the agent may invoke; the host issues a `canvasInvokeAction` request to the provider and returns its opaque result. Opening and closing are the same shape (`canvasOpen`, `canvasClose`). For a host-provided canvas the host resolves these internally and sends no request. +- **The message bridge.** `canvas/message` relays an opaque payload between the rendered View and the provider in either direction — the relay-carried analogue of a `postMessage` bridge. Like `terminal/input`, it is a pure signal with a no-op reducer, so it never accumulates in state. + +Closing follows the same signal-then-request pattern: the user hits ✕, the renderer dispatches `canvas/closeRequested`, and the host runs the close flow — resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. + +## Lifecycle + +1. A client declares `ClientCapabilities.canvas` and publishes any `canvasProviders`. The host folds them into `SessionState.canvases`. +2. The agent opens a canvas. The host mints an instance, sends `canvasOpen` to the provider (or resolves it internally), and adds an `OpenCanvasRef` to `SessionState.openCanvases`. +3. A renderer subscribes to the instance's `ahp-canvas:/` channel, receives the `CanvasState` snapshot, and loads its `url`. +4. The provider pushes `canvas/updated` as the canvas evolves; the agent invokes declared actions; the View and provider exchange `canvas/message` payloads. +5. The user (or the agent) closes the canvas. The host resolves `canvasClose` and republishes `openCanvases` without the instance; subscribers see the channel go away. + +See the [Canvas Channel specification](/specification/canvas-channel) for the exact state shapes, action semantics, command signatures, and error codes. diff --git a/docs/specification/canvas-channel.md b/docs/specification/canvas-channel.md new file mode 100644 index 00000000..a1ab74b6 --- /dev/null +++ b/docs/specification/canvas-channel.md @@ -0,0 +1,208 @@ +# Canvas Channel + +A canvas is a rich, interactive UI surface the agent can open alongside a session — a document editor, a diff view, a live preview, a spreadsheet, a browser pane. Each open canvas is a first-class subscribable resource with its own `ahp-canvas:/` channel, mirroring the "one channel per resource" convention used by terminals, changesets, and resource watches. + +Rendering is **state-driven**: a client renders a canvas by reading its [`CanvasState.url`](#state) and resolving that address per the renderer's URL policy. The host never sends a "render this" request — it publishes state, and the renderer reacts. Interaction flows back to whichever peer *provides* the canvas through a small server ↔ client request family (`canvasOpen` / `canvasInvokeAction` / `canvasClose`) and an opaque `canvas/message` bridge. + +## Capability + +The canvas surface is entirely opt-in. A client advertises it during the handshake: + +```typescript +ClientCapabilities { + canvas?: {} // presence = "I can render canvases and host canvas providers" +} +``` + +A client that declares `canvas` can both **render** an opaque canvas URL in an isolated surface and **provide** canvases — answering `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for the providers it publishes. Hosts SHOULD only populate [`SessionState.canvases`](#discovery) / [`SessionState.openCanvases`](#discovery) and only route canvas requests to a client that declared the capability. Clients that omit it see no canvas surface at all. + +## Discovery + +Two session-state fields expose the canvas surface. Both use full-replacement semantics and are populated only while at least one connected client has declared the capability: + +```typescript +SessionState { + canvases?: SessionCanvasDeclaration[] // what the agent can open + openCanvases?: OpenCanvasRef[] // what is open right now +} +``` + +- **`canvases`** is the aggregated registry of every canvas the agent may open, replaced wholesale via the `session/canvasesChanged` action. It is the union of server-side declarations and the providers each connected client publishes. +- **`openCanvases`** is the lightweight catalogue of currently-open instances, replaced wholesale via `session/openCanvasesChanged`. Each entry carries the instance's channel URI so a subscriber can render it without subscribing to every instance. + +A client contributes providers by publishing them on its active-client entry: + +```typescript +SessionActiveClient { + canvasProviders?: ClientCanvasDeclaration[] +} +``` + +The host folds each `ClientCanvasDeclaration` into `canvases`, filling in the owning `extensionId` and setting `source` to the client variant so it knows where to route later requests. + +### Declarations + +```typescript +// One entry in the aggregated SessionState.canvases registry. +SessionCanvasDeclaration { + extensionId: string // owning provider id + extensionName?: string + canvasId: string // provider-local id, unique within extensionId + displayName: string + description: string + inputSchema?: object // JSON Schema for the open input (opaque to AHP) + actions?: SessionCanvasAction[] + source: CanvasProviderSource // where it came from — for routing and cleanup +} + +// The lighter shape a client publishes; the host derives extensionId + source. +ClientCanvasDeclaration { + canvasId: string + displayName: string + description: string + inputSchema?: object + actions?: SessionCanvasAction[] +} + +// One named action a canvas exposes to the agent. +SessionCanvasAction { + name: string // unique within the owning (extensionId, canvasId) + description?: string + inputSchema?: object +} +``` + +`CanvasProviderSource` is a discriminated union over `CanvasProviderKind` recording who owns the callbacks for a canvas: + +```typescript +CanvasServerProviderSource { kind: 'server' } // the host provides it +CanvasClientProviderSource { kind: 'client', clientId } // a connected client provides it +``` + +### Open-instance catalogue + +Each open instance is summarised by an `OpenCanvasRef`, which carries the `ahp-canvas:/` URI to subscribe to: + +```typescript +OpenCanvasRef { + instanceId: string + channel: URI // ahp-canvas:/ — subscribe for full CanvasState + canvasId: string + extensionId: string + extensionName?: string + title?: string // mirrored from CanvasState.title + availability: CanvasAvailability +} +``` + +## URI + +``` +ahp-canvas:/ +``` + +The id is **server-assigned**. The host allocates a fresh channel URI when it opens an instance and surfaces it on `OpenCanvasRef.channel`; callers MUST treat the URI as opaque. + +## State + +Subscribing to an instance's channel yields the authoritative, mutable per-instance view: + +```typescript +CanvasState { + instanceId: string // server-assigned handle, unique within the session + canvasId: string // provider-local id this instance was opened from + extensionId: string // owning provider id + extensionName?: string + displayName?: string + input?: object // the open input, retained for resume/rebind + title?: string + status?: string // provider-defined, opaque to AHP + url?: string // renderer-targeted content address (see Rendering) + availability: CanvasAvailability + provider: CanvasProviderSource // which provider owns this instance's callbacks +} + +CanvasAvailability = 'ready' | 'stale' // 'stale' = provider currently unavailable +``` + +## Rendering and content + +A renderer displays a canvas by dispatching on the scheme of `CanvasState.url`: + +- A **directly-loadable** address — `https:`, an in-process scheme, or `http://localhost` — is loaded straight into the isolated surface, subject to the renderer's own URL policy. +- An `ahp-canvas-content://` address is **channel-served**: in a relayed deployment the renderer cannot dial the host directly, so it resolves the bytes over the instance's existing channel with [`canvasReadResource`](#commands) instead of loading a network URL. The `` segment names which canvas channel to read from. Sub-resources the loaded document references (stylesheets, images) are fetched the same way. + +The canvas content itself is opaque to AHP — the protocol carries only the address and, when channel-served, the resolved bytes. + +## Actions + +Three actions travel on the per-instance channel. Because the channel is scoped to a single instance by the action envelope, none of them carry an `instanceId`. + +| Action | Client-dispatchable | Reducer effect | +|---|:---:|---| +| `canvas/updated` | No | Sparse-merges `title` / `status` / `url` / `availability` into `CanvasState`. | +| `canvas/closeRequested` | Yes | No-op — signals the host to run the close flow. | +| `canvas/message` | Yes | No-op — opaque View ↔ provider message bridge. | + +`canvas/updated` uses **sparse-merge** semantics: each present field overwrites the corresponding `CanvasState` field, and an absent field preserves the current value. There is no clear-to-absent through this action — a provider that needs to reset a field re-publishes it, and a full reset arrives as a fresh `CanvasState` snapshot on (re)subscribe. + +`canvas/closeRequested` and `canvas/message` are pure signals with no-op reducers, mirroring how `terminal/input` is a side-effect-only client action — they never bloat channel state. `canvas/closeRequested` is a client → host signal (the user hit ✕); the host responds by resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. `canvas/message` is bidirectional: a client dispatches it to carry a View → provider message, and the host emits it to carry a provider → View message. + +## Provider request family + +When the agent opens, drives, or closes a canvas whose provider is a client, the host issues a request to that client. For a server-side provider the host resolves the operation host-internally and emits no request. Following the `resource*` precedent, all three methods are registered in the server → client command map and mirrored in the client → server map for symmetry; a client normally never initiates them, and a receiver SHOULD reject a request whose target is not one of its declared providers. + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant H as Host (server) + participant C as Client (provider + renderer) + + Note over C: declares ClientCapabilities.canvas
publishes canvasProviders + C->>H: dispatchAction session/activeClientsChanged (canvasProviders) + H->>C: action session/canvasesChanged (registry) + + A->>H: open canvas "diff" + H->>C: canvasOpen { canvasId, extensionId, instanceId, input } + C-->>H: { url, title } + H->>C: action session/openCanvasesChanged (instance added) + C->>H: subscribe { channel: "ahp-canvas:/abc" } + H-->>C: snapshot { state: CanvasState } + + Note over C: renderer loads CanvasState.url + A->>H: invoke action "format" + H->>C: canvasInvokeAction { instanceId, actionName, input } + C-->>H: { value } + C-->>H: action canvas/updated { status } + + C->>H: action canvas/closeRequested + H->>C: canvasClose { instanceId } + H->>C: action session/openCanvasesChanged (instance removed) +``` + +### Commands + +| Method | Channel | Direction | Purpose | +|---|---|---|---| +| `canvasOpen` | `ahp-session:/` | Client ↔ Server | Open an instance against its provider. Returns the initial `url` / `title` / `status`. | +| `canvasInvokeAction` | `ahp-session:/` | Client ↔ Server | Invoke a declared action on an open instance. Returns an opaque provider value. | +| `canvasClose` | `ahp-session:/` | Client ↔ Server | Close an instance against its provider, as part of the close flow. | +| `canvasReadResource` | `ahp-canvas:/` | Client → Server | Resolve an `ahp-canvas-content:` address over the instance's channel. Returns `contents: CanvasResourceContent[]`. | + +`canvasReadResource` is modeled on MCP's `resources/read`; each `CanvasResourceContent` carries the resolved `uri`, an optional `mimeType`, and exactly one of `text` (UTF-8) or `blob` (base64). + +### Actions + +| Action | Direction | +|---|---| +| `canvas/updated` | host → client (over the standard `action` envelope) | +| `canvas/closeRequested` | client → host | +| `canvas/message` | client → host **or** host → client | + +## Errors + +| Code | Name | When | +|---|---|---| +| `-32012` | `CanvasProviderError` | A `canvasOpen` / `canvasInvokeAction` / `canvasClose` request failed. The `data` field MUST be a `CanvasProviderErrorData` carrying the provider-defined `{ code, message }` — for example `canvas_action_no_handler` when the provider declared the canvas but has no handler for the action, or `canvas_provider_unavailable` when the provider is disconnected. | +| `-32008` | `NotFound` | A `canvasReadResource` content URI does not resolve. | diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index 95cd5d16..55240d19 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -86,6 +86,15 @@ Each entry is a [`SessionInputRequest`](/reference/session#sessioninputrequest) Every entry carries the owning `chat` URI plus the identifiers (`request.id`, or `turnId` + `toolCall.toolCallId`) needed to construct the response. A client therefore answers by dispatching the ordinary `chat/*` action **to that chat's channel** — it does **not** need to have subscribed to the chat first. `inputNeeded` is a read/respond convenience surface, not a separate response protocol: the chat channel remains the source of truth and the host removes the aggregate entry once the chat-level request resolves. +### Canvas discovery + +When at least one connected client declares [`ClientCapabilities.canvas`](/specification/canvas-channel#capability), the session surfaces the canvas registry and the open-instance catalogue in its state: + +- `SessionState.canvases` — the aggregated registry of every canvas the agent may open, replaced wholesale via `session/canvasesChanged`. The host builds it from server-side declarations plus the providers each client publishes on [`SessionActiveClient.canvasProviders`](/reference/session#sessionactiveclient). +- `SessionState.openCanvases` — the catalogue of currently-open instances, replaced wholesale via `session/openCanvasesChanged`. Each `OpenCanvasRef` carries an `ahp-canvas:/` channel URI a subscriber renders without subscribing to every instance. + +Both actions are server-only; a client contributes to the registry by publishing `canvasProviders` on its active-client entry, not by dispatching either action. The per-instance channel, its `CanvasState`, and the provider request family are specified on the [Canvas Channel](/specification/canvas-channel) page. + ### Disposal ```jsonc diff --git a/docs/specification/subscriptions.md b/docs/specification/subscriptions.md index 50d1fe70..47fa127a 100644 --- a/docs/specification/subscriptions.md +++ b/docs/specification/subscriptions.md @@ -8,9 +8,10 @@ The channel concept is woven into every wire message. **Every command and every | Direction | Methods | `channel` value | |---|---|---| -| Client → Server commands (channel-scoped) | `subscribe`, `unsubscribe`, `createSession`, `disposeSession`, `createTerminal`, `disposeTerminal`, `fetchTurns`, `completions`, `invokeChangesetOperation` | The target channel's URI (e.g. `ahp-session:/`). | +| Client → Server commands (channel-scoped) | `subscribe`, `unsubscribe`, `createSession`, `disposeSession`, `createTerminal`, `disposeTerminal`, `fetchTurns`, `completions`, `invokeChangesetOperation`, `canvasReadResource` | The target channel's URI (e.g. `ahp-session:/`, or `ahp-canvas:/` for `canvasReadResource`). | | Client → Server commands (connection-level) | `initialize`, `ping`, `reconnect`, `listSessions`, `authenticate`, `resolveSessionConfig`, `sessionConfigCompletions`, `resourceRead`, `resourceWrite`, `resourceList`, `resourceCopy`, `resourceDelete`, `resourceMove`, `resourceResolve`, `resourceMkdir`, `resourceRequest`, `createResourceWatch` | Literal `'ahp-root://'`. | | Server → Client commands (bidirectional `resource*` family) | The same nine `resource*` request methods plus `createResourceWatch` may also be initiated by the server. Used for host-driven per-session filesystem providers and for fetching client-published URIs (e.g. `virtual://my-client/...` plugins). | Literal `'ahp-root://'`. | +| Server → Client commands (canvas provider family) | `canvasOpen`, `canvasInvokeAction`, `canvasClose` — initiated by the host against the client that declared the target canvas provider; mirrored in the client → server map for symmetry. See [Canvas Channel](/specification/canvas-channel). | The owning `ahp-session:/` URI. | | Client → Server `dispatchAction` | The channel the action targets. | | Server → Client `action` | The channel that owns the action envelope. | | Server → Client protocol notifications | `root/sessionAdded`, `root/sessionRemoved`, `root/sessionSummaryChanged`, `auth/required`, `otlp/exportLogs`, `otlp/exportTraces`, `otlp/exportMetrics` | The channel the notification scopes to (the root channel for `root/*`; the channel the auth requirement targets for `auth/required`; the host-defined `ahp-otlp:` channel URI for `otlp/*`). | @@ -29,6 +30,7 @@ The rest of this page details the URI scheme and the lifecycle of a subscription | `ahp-changeset:/` | `ChangesetState` | Per-changeset state. URI is obtained by expanding a `Changeset.uriTemplate` advertised on a session; the id is server-defined. | | `ahp-otlp:` _(authority/path host-defined)_ | _stateless_ | OpenTelemetry signal channels (logs, traces, metrics). Concrete URIs are advertised on `InitializeResult.telemetry`; clients MUST treat them as opaque. See [Telemetry Channel](/specification/telemetry-channel). | | `ahp-resource-watch:/` | `ResourceWatchState` | Per-watch channel returned by `createResourceWatch`. Delivers `resourceWatch/changed` actions for file/directory changes under the watched URI. The id is caller-chosen. | +| `ahp-canvas:/` | `CanvasState` | Per-open-canvas-instance state. URI is server-assigned and surfaced on `OpenCanvasRef.channel`; the id is opaque. See [Canvas Channel](/specification/canvas-channel). | Future channel types (LSP relay, MCP relay, …) introduce their own URI schemes. Clients MUST NOT subscribe to a scheme they do not understand. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 62e065d9..15b0540a 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -405,6 +405,46 @@ "clientId" ] }, + "SessionCanvasesChangedAction": { + "type": "object", + "description": "The aggregated canvas registry for this session changed.\n\nFull-replacement semantics: the `canvases` array replaces\n{@link SessionState.canvases} entirely, mirroring\n`session/serverToolsChanged`. The host republishes the union of every\nconnected provider (server-side and client-declared) whenever it changes.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionCanvasesChanged" + }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Updated canvas registry (full replacement)." + } + }, + "required": [ + "type", + "canvases" + ] + }, + "SessionOpenCanvasesChangedAction": { + "type": "object", + "description": "The catalogue of open canvas instances for this session changed.\n\nFull-replacement semantics: the `openCanvases` array replaces\n{@link SessionState.openCanvases} entirely. The host republishes the\ncatalogue as instances open and close.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionOpenCanvasesChanged" + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Updated open-instance catalogue (full replacement)." + } + }, + "required": [ + "type", + "openCanvases" + ] + }, "SessionInputNeededSetAction": { "type": "object", "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", @@ -1842,6 +1882,62 @@ "changes" ] }, + "CanvasUpdatedAction": { + "type": "object", + "description": "The canvas instance's presentation state changed.\n\nSparse-merge semantics: each present field overwrites the corresponding\n{@link CanvasState} field, and an absent field preserves the current value.\nThere is no clear-to-absent via this action — that three-state distinction\ncannot survive JSON transport uniformly across languages, so a provider that\nneeds to reset a field re-publishes it, and a full reset arrives as a fresh\n{@link CanvasState} snapshot on (re)subscribe.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.CanvasUpdated" + }, + "title": { + "type": "string", + "description": "New title. Absent preserves the current title." + }, + "status": { + "type": "string", + "description": "New provider-defined status. Absent preserves the current status." + }, + "url": { + "type": "string", + "description": "New content address. Absent preserves the current url." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "New availability. Absent preserves the current availability." + } + }, + "required": [ + "type" + ] + }, + "CanvasCloseRequestedAction": { + "type": "object", + "description": "The user asked to close this canvas (e.g. hit the ✕ on the surface).\n\nA pure client→host signal with a no-op reducer, mirroring how\n`terminal/input` is a side-effect-only client action. The host runs the\nclose flow in response — resolving `canvasClose` against the provider and\ndropping the instance from {@link SessionState.openCanvases} — rather than\nthe reducer mutating channel state.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.CanvasCloseRequested" + } + }, + "required": [ + "type" + ] + }, + "CanvasMessageAction": { + "type": "object", + "description": "An opaque message relayed between the rendered canvas View and the\ninstance's provider — the relay-carried analogue of a `postMessage` bridge.\n\nBidirectional: a client dispatches it to carry a View→provider message, and\nthe host emits it to carry a provider→View message (routed to the provider\nresolved for the instance, or handled host-internally for a server-side\nprovider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it\nis a pure signal with a no-op reducer, so it never bloats channel state. See\n{@link /specification/canvas-channel | Canvas Channel}.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.CanvasMessage" + }, + "payload": { + "description": "Opaque, provider-defined message payload." + } + }, + "required": [ + "type", + "payload" + ] + }, "ChatToolCallConfirmedAction": { "oneOf": [ {}, @@ -2349,6 +2445,9 @@ { "$ref": "#/$defs/ResourceWatchState" }, + { + "$ref": "#/$defs/CanvasState" + }, { "$ref": "#/$defs/AnnotationsState" }, @@ -2692,6 +2791,20 @@ }, "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Aggregated canvas registry currently exposed to the agent — the union of\nevery connected provider (server-side and client-declared). Each entry\ndescribes a canvas the agent can open; the host folds\n{@link SessionActiveClient.canvasProviders | client-declared providers}\ninto this list alongside its own server-side providers.\n\nFull-replacement via `session/canvasesChanged`. Populated only when at\nleast one connected client declared {@link ClientCapabilities.canvas};\nabsent for sessions with no canvas surface. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Lightweight catalogue of currently-open canvas instances. Each entry\ncarries the instance's `ahp-canvas:/` channel URI so a subscriber can\nsubscribe to the full {@link CanvasState} and render it — analogous to how\n{@link RootState.terminals} catalogues live terminals whose full state\nlives on each terminal channel.\n\nFull-replacement via `session/openCanvasesChanged`." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -2732,6 +2845,13 @@ "$ref": "#/$defs/ClientPluginCustomization" }, "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + }, + "canvasProviders": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientCanvasDeclaration" + }, + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -2739,6 +2859,182 @@ "tools" ] }, + "CanvasServerProviderSource": { + "type": "object", + "description": "A canvas provided by the host. Carries no `clientId` — server-side provider\nrequests are resolved host-internally rather than routed to a peer.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Server" + } + }, + "required": [ + "kind" + ] + }, + "CanvasClientProviderSource": { + "type": "object", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Client" + }, + "clientId": { + "type": "string", + "description": "`clientId` of the providing client (matches `initialize`)." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "SessionCanvasAction": { + "type": "object", + "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "properties": { + "name": { + "type": "string", + "description": "Action name, unique within the owning `(extensionId, canvasId)`." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the action does." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + } + }, + "required": [ + "name" + ] + }, + "SessionCanvasDeclaration": { + "type": "object", + "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", + "properties": { + "extensionId": { + "type": "string", + "description": "Owning provider id. Stable across declarations and instances." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id. Unique within `extensionId`." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + }, + "source": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Where the declaration came from — for routing and cleanup." + } + }, + "required": [ + "extensionId", + "canvasId", + "displayName", + "description", + "source" + ] + }, + "ClientCanvasDeclaration": { + "type": "object", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "properties": { + "canvasId": { + "type": "string", + "description": "Provider-local canvas id, unique within the publishing client." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + } + }, + "required": [ + "canvasId", + "displayName", + "description" + ] + }, + "OpenCanvasRef": { + "type": "object", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "title": { + "type": "string", + "description": "Current instance title, mirrored from {@link CanvasState.title}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether the instance's provider is currently available." + } + }, + "required": [ + "instanceId", + "channel", + "canvasId", + "extensionId", + "availability" + ] + }, "SessionInputRequestBase": { "type": "object", "description": "Fields common to every {@link SessionInputRequest} variant.", @@ -6249,6 +6545,64 @@ "type" ] }, + "CanvasState": { + "type": "object", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Input the agent supplied when opening the instance. Retained so the\ninstance can be resumed or rebound after a reconnect." + }, + "title": { + "type": "string", + "description": "Current instance title." + }, + "status": { + "type": "string", + "description": "Provider-defined status string (opaque to AHP)." + }, + "url": { + "type": "string", + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether this instance's provider is currently available." + }, + "provider": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Which provider owns the callbacks (`canvasOpen` / … ) for this instance." + } + }, + "required": [ + "instanceId", + "canvasId", + "extensionId", + "availability", + "provider" + ] + }, "StringOrMarkdown": { "oneOf": [ { @@ -6283,6 +6637,18 @@ ], "description": "A primitive JSON value: a string, number, boolean, or `null`." }, + "CanvasProviderSource": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/CanvasServerProviderSource" + }, + { + "$ref": "#/$defs/CanvasClientProviderSource" + } + ], + "description": "Where a canvas declaration came from — used for request routing and for\ncleaning the entry up when its provider disconnects. Modeled as a\ndiscriminated union so the `clientId` is only present (and required) for the\nclient variant." + }, "SessionInputRequest": { "oneOf": [ {}, @@ -6866,6 +7232,21 @@ }, { "$ref": "#/$defs/ResourceWatchChangedAction" + }, + { + "$ref": "#/$defs/SessionCanvasesChangedAction" + }, + { + "$ref": "#/$defs/SessionOpenCanvasesChangedAction" + }, + { + "$ref": "#/$defs/CanvasUpdatedAction" + }, + { + "$ref": "#/$defs/CanvasCloseRequestedAction" + }, + { + "$ref": "#/$defs/CanvasMessageAction" } ] } diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 0136d547..f7f5e57c 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -93,6 +93,11 @@ "type": "object", "additionalProperties": {}, "description": "Client can render\n[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.\nit can host the View sandbox, run the `ui/*` protocol against it,\nand forward `mcp://`-channel traffic on the App's behalf.\n\nHosts SHOULD only populate\n{@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}\n(and expose the corresponding\n{@link McpServerCustomization.channel | `mcp://` channel}) when this\ncapability is declared. Clients that omit it MUST treat\nApp-bearing tool calls as ordinary MCP tool calls." + }, + "canvas": { + "type": "object", + "additionalProperties": {}, + "description": "Client can render canvases and host client-declared canvas providers — it\ncan render an opaque canvas URL in an isolated surface, and it can answer\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases\nit declares via {@link SessionActiveClient.canvasProviders}.\n\nHosts SHOULD only populate {@link SessionState.canvases} /\n{@link SessionState.openCanvases} and only route canvas requests to a\nclient that declared this capability. Clients that omit it see no canvas\nsurface. See {@link /specification/canvas-channel | Canvas Channel}." } } }, @@ -1276,6 +1281,191 @@ "channel" ] }, + "CanvasOpenParams": { + "type": "object", + "description": "Opens a canvas instance against its provider.\n\nSent by the host to the client that declared the target canvas via\n{@link SessionActiveClient.canvasProviders} (a client that also declared\n{@link ClientCapabilities.canvas}). For a server-side provider the host\nresolves the open host-internally and emits no request. The provider returns\nthe initial render target and presentation fields, which the host folds into\nthe new instance's {@link CanvasState}.\n\nMirrors the `resource*` precedent: registered in `ServerCommandMap` and\nmirrored in `CommandMap` for symmetry. A client normally never initiates it —\nthe host is not a canvas provider — and a receiver SHOULD reject a request\nwhose target is not one of its declared providers.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning session channel URI (`ahp-session:/`)." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id to open." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "instanceId": { + "type": "string", + "description": "Caller-minted handle for the new instance." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Open input, validated by the provider against its declared schema." + } + }, + "required": [ + "channel", + "canvasId", + "extensionId", + "instanceId" + ] + }, + "CanvasOpenResult": { + "type": "object", + "description": "Result of the `canvasOpen` command.", + "properties": { + "url": { + "type": "string", + "description": "Initial content address for the instance (see {@link CanvasState.url})." + }, + "title": { + "type": "string", + "description": "Initial title." + }, + "status": { + "type": "string", + "description": "Initial provider-defined status." + } + } + }, + "CanvasInvokeActionParams": { + "type": "object", + "description": "Invokes one of a canvas's declared actions against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) when the agent invokes a\n{@link SessionCanvasAction | declared action} on an open instance. The\nprovider returns an opaque, provider-defined value. Registered symmetrically\nwith the rest of the provider family (see {@link CanvasOpenParams}).", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning session channel URI (`ahp-session:/`)." + }, + "instanceId": { + "type": "string", + "description": "Instance handle the action targets." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id of the instance." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "actionName": { + "type": "string", + "description": "Declared action name to invoke." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Action input, validated by the provider against its declared schema." + } + }, + "required": [ + "channel", + "instanceId", + "canvasId", + "extensionId", + "actionName" + ] + }, + "CanvasInvokeActionResult": { + "type": "object", + "description": "Result of the `canvasInvokeAction` command.", + "properties": { + "value": { + "description": "Opaque, provider-defined return value." + } + } + }, + "CanvasCloseParams": { + "type": "object", + "description": "Closes a canvas instance against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) as part of the close flow — typically after a client\ndispatches `canvas/closeRequested`. The host then drops the instance from\n{@link SessionState.openCanvases}. Registered symmetrically with the rest of\nthe provider family (see {@link CanvasOpenParams}).", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning session channel URI (`ahp-session:/`)." + }, + "instanceId": { + "type": "string", + "description": "Instance handle to close." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id of the instance." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + } + }, + "required": [ + "channel", + "instanceId", + "canvasId", + "extensionId" + ] + }, + "CanvasReadResourceParams": { + "type": "object", + "description": "Reads channel-served canvas content by `ahp-canvas-content:` URI.\n\nA client → host request, modeled on MCP's `resources/read`. When a canvas's\n{@link CanvasState.url} (or a sub-resource the rendered document references)\nis an `ahp-canvas-content://` address, the renderer cannot\ndial the host directly — for example in a relayed deployment behind a broker\n— so it resolves the bytes over the instance's existing `ahp-canvas:/`\nchannel with this request instead of loading a network URL. The ``\nsegment of the URI identifies which canvas channel to read from. See\n{@link /specification/canvas-channel | Canvas Channel}.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning canvas channel URI (`ahp-canvas:/`)." + }, + "uri": { + "type": "string", + "description": "An `ahp-canvas-content://` content URI to read." + } + }, + "required": [ + "channel", + "uri" + ] + }, + "CanvasReadResourceResult": { + "type": "object", + "description": "Result of the `canvasReadResource` command.", + "properties": { + "contents": { + "type": "array", + "items": { + "$ref": "#/$defs/CanvasResourceContent" + }, + "description": "The resolved content parts, wrapped for forward compatibility." + } + }, + "required": [ + "contents" + ] + }, + "CanvasResourceContent": { + "type": "object", + "description": "One resolved piece of channel-served canvas content.\n\nCarries exactly one of {@link text} (text payloads) or {@link blob}\n(base64-encoded binary payloads).", + "properties": { + "uri": { + "type": "string", + "description": "The content URI this part resolves." + }, + "mimeType": { + "type": "string", + "description": "MIME type of the content, when known." + }, + "text": { + "type": "string", + "description": "UTF-8 text content, for text payloads." + }, + "blob": { + "type": "string", + "description": "Base64-encoded content, for binary payloads." + } + }, + "required": [ + "uri" + ] + }, "Icon": { "type": "object", "description": "An optionally-sized icon that can be displayed in a user interface.", @@ -1694,6 +1884,9 @@ { "$ref": "#/$defs/ResourceWatchState" }, + { + "$ref": "#/$defs/CanvasState" + }, { "$ref": "#/$defs/AnnotationsState" }, @@ -2037,6 +2230,20 @@ }, "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Aggregated canvas registry currently exposed to the agent — the union of\nevery connected provider (server-side and client-declared). Each entry\ndescribes a canvas the agent can open; the host folds\n{@link SessionActiveClient.canvasProviders | client-declared providers}\ninto this list alongside its own server-side providers.\n\nFull-replacement via `session/canvasesChanged`. Populated only when at\nleast one connected client declared {@link ClientCapabilities.canvas};\nabsent for sessions with no canvas surface. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Lightweight catalogue of currently-open canvas instances. Each entry\ncarries the instance's `ahp-canvas:/` channel URI so a subscriber can\nsubscribe to the full {@link CanvasState} and render it — analogous to how\n{@link RootState.terminals} catalogues live terminals whose full state\nlives on each terminal channel.\n\nFull-replacement via `session/openCanvasesChanged`." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -2077,6 +2284,13 @@ "$ref": "#/$defs/ClientPluginCustomization" }, "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + }, + "canvasProviders": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientCanvasDeclaration" + }, + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -2084,6 +2298,182 @@ "tools" ] }, + "CanvasServerProviderSource": { + "type": "object", + "description": "A canvas provided by the host. Carries no `clientId` — server-side provider\nrequests are resolved host-internally rather than routed to a peer.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Server" + } + }, + "required": [ + "kind" + ] + }, + "CanvasClientProviderSource": { + "type": "object", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Client" + }, + "clientId": { + "type": "string", + "description": "`clientId` of the providing client (matches `initialize`)." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "SessionCanvasAction": { + "type": "object", + "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "properties": { + "name": { + "type": "string", + "description": "Action name, unique within the owning `(extensionId, canvasId)`." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the action does." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + } + }, + "required": [ + "name" + ] + }, + "SessionCanvasDeclaration": { + "type": "object", + "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", + "properties": { + "extensionId": { + "type": "string", + "description": "Owning provider id. Stable across declarations and instances." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id. Unique within `extensionId`." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + }, + "source": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Where the declaration came from — for routing and cleanup." + } + }, + "required": [ + "extensionId", + "canvasId", + "displayName", + "description", + "source" + ] + }, + "ClientCanvasDeclaration": { + "type": "object", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "properties": { + "canvasId": { + "type": "string", + "description": "Provider-local canvas id, unique within the publishing client." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + } + }, + "required": [ + "canvasId", + "displayName", + "description" + ] + }, + "OpenCanvasRef": { + "type": "object", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "title": { + "type": "string", + "description": "Current instance title, mirrored from {@link CanvasState.title}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether the instance's provider is currently available." + } + }, + "required": [ + "instanceId", + "channel", + "canvasId", + "extensionId", + "availability" + ] + }, "SessionInputRequestBase": { "type": "object", "description": "Fields common to every {@link SessionInputRequest} variant.", @@ -5594,6 +5984,64 @@ "type" ] }, + "CanvasState": { + "type": "object", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Input the agent supplied when opening the instance. Retained so the\ninstance can be resumed or rebound after a reconnect." + }, + "title": { + "type": "string", + "description": "Current instance title." + }, + "status": { + "type": "string", + "description": "Provider-defined status string (opaque to AHP)." + }, + "url": { + "type": "string", + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether this instance's provider is currently available." + }, + "provider": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Which provider owns the callbacks (`canvasOpen` / … ) for this instance." + } + }, + "required": [ + "instanceId", + "canvasId", + "extensionId", + "availability", + "provider" + ] + }, "ActionOrigin": { "type": "object", "description": "Identifies the client that originally dispatched an action.", @@ -5994,6 +6442,46 @@ "clientId" ] }, + "SessionCanvasesChangedAction": { + "type": "object", + "description": "The aggregated canvas registry for this session changed.\n\nFull-replacement semantics: the `canvases` array replaces\n{@link SessionState.canvases} entirely, mirroring\n`session/serverToolsChanged`. The host republishes the union of every\nconnected provider (server-side and client-declared) whenever it changes.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionCanvasesChanged" + }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Updated canvas registry (full replacement)." + } + }, + "required": [ + "type", + "canvases" + ] + }, + "SessionOpenCanvasesChangedAction": { + "type": "object", + "description": "The catalogue of open canvas instances for this session changed.\n\nFull-replacement semantics: the `openCanvases` array replaces\n{@link SessionState.openCanvases} entirely. The host republishes the\ncatalogue as instances open and close.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionOpenCanvasesChanged" + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Updated open-instance catalogue (full replacement)." + } + }, + "required": [ + "type", + "openCanvases" + ] + }, "SessionInputNeededSetAction": { "type": "object", "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", @@ -7430,6 +7918,62 @@ "type", "changes" ] + }, + "CanvasUpdatedAction": { + "type": "object", + "description": "The canvas instance's presentation state changed.\n\nSparse-merge semantics: each present field overwrites the corresponding\n{@link CanvasState} field, and an absent field preserves the current value.\nThere is no clear-to-absent via this action — that three-state distinction\ncannot survive JSON transport uniformly across languages, so a provider that\nneeds to reset a field re-publishes it, and a full reset arrives as a fresh\n{@link CanvasState} snapshot on (re)subscribe.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.CanvasUpdated" + }, + "title": { + "type": "string", + "description": "New title. Absent preserves the current title." + }, + "status": { + "type": "string", + "description": "New provider-defined status. Absent preserves the current status." + }, + "url": { + "type": "string", + "description": "New content address. Absent preserves the current url." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "New availability. Absent preserves the current availability." + } + }, + "required": [ + "type" + ] + }, + "CanvasCloseRequestedAction": { + "type": "object", + "description": "The user asked to close this canvas (e.g. hit the ✕ on the surface).\n\nA pure client→host signal with a no-op reducer, mirroring how\n`terminal/input` is a side-effect-only client action. The host runs the\nclose flow in response — resolving `canvasClose` against the provider and\ndropping the instance from {@link SessionState.openCanvases} — rather than\nthe reducer mutating channel state.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.CanvasCloseRequested" + } + }, + "required": [ + "type" + ] + }, + "CanvasMessageAction": { + "type": "object", + "description": "An opaque message relayed between the rendered canvas View and the\ninstance's provider — the relay-carried analogue of a `postMessage` bridge.\n\nBidirectional: a client dispatches it to carry a View→provider message, and\nthe host emits it to carry a provider→View message (routed to the provider\nresolved for the instance, or handled host-internally for a server-side\nprovider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it\nis a pure signal with a no-op reducer, so it never bloats channel state. See\n{@link /specification/canvas-channel | Canvas Channel}.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.CanvasMessage" + }, + "payload": { + "description": "Opaque, provider-defined message payload." + } + }, + "required": [ + "type", + "payload" + ] } } } diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 08d3863e..85151dc7 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -74,6 +74,24 @@ "supportedVersions" ] }, + "CanvasProviderErrorData": { + "type": "object", + "description": "Details carried in the `data` field of a `CanvasProviderError` (-32012)\nerror.\n\nThe `code` is a provider-defined string (opaque to AHP) identifying the\nfailure — observed values include `canvas_action_no_handler` and\n`canvas_provider_unavailable`. `message` is a human-readable description.", + "properties": { + "code": { + "type": "string", + "description": "Provider-defined error code identifying the failure." + }, + "message": { + "type": "string", + "description": "Human-readable error message." + } + }, + "required": [ + "code", + "message" + ] + }, "AhpErrorDetailsMap": { "type": "object", "description": "Maps each AHP error code that carries structured `data` to the type of\nthat data.\n\nError codes not present in this map either have no `data` payload or\ncarry an unspecified payload that callers SHOULD treat as `unknown`.", @@ -86,12 +104,16 @@ }, "[AhpErrorCodes.UnsupportedProtocolVersion]": { "$ref": "#/$defs/UnsupportedProtocolVersionErrorData" + }, + "[AhpErrorCodes.CanvasProviderError]": { + "$ref": "#/$defs/CanvasProviderErrorData" } }, "required": [ "[AhpErrorCodes.AuthRequired]", "[AhpErrorCodes.PermissionDenied]", - "[AhpErrorCodes.UnsupportedProtocolVersion]" + "[AhpErrorCodes.UnsupportedProtocolVersion]", + "[AhpErrorCodes.CanvasProviderError]" ] }, "Icon": { @@ -512,6 +534,9 @@ { "$ref": "#/$defs/ResourceWatchState" }, + { + "$ref": "#/$defs/CanvasState" + }, { "$ref": "#/$defs/AnnotationsState" }, @@ -855,6 +880,20 @@ }, "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Aggregated canvas registry currently exposed to the agent — the union of\nevery connected provider (server-side and client-declared). Each entry\ndescribes a canvas the agent can open; the host folds\n{@link SessionActiveClient.canvasProviders | client-declared providers}\ninto this list alongside its own server-side providers.\n\nFull-replacement via `session/canvasesChanged`. Populated only when at\nleast one connected client declared {@link ClientCapabilities.canvas};\nabsent for sessions with no canvas surface. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Lightweight catalogue of currently-open canvas instances. Each entry\ncarries the instance's `ahp-canvas:/` channel URI so a subscriber can\nsubscribe to the full {@link CanvasState} and render it — analogous to how\n{@link RootState.terminals} catalogues live terminals whose full state\nlives on each terminal channel.\n\nFull-replacement via `session/openCanvasesChanged`." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -895,6 +934,13 @@ "$ref": "#/$defs/ClientPluginCustomization" }, "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + }, + "canvasProviders": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientCanvasDeclaration" + }, + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -902,6 +948,182 @@ "tools" ] }, + "CanvasServerProviderSource": { + "type": "object", + "description": "A canvas provided by the host. Carries no `clientId` — server-side provider\nrequests are resolved host-internally rather than routed to a peer.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Server" + } + }, + "required": [ + "kind" + ] + }, + "CanvasClientProviderSource": { + "type": "object", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Client" + }, + "clientId": { + "type": "string", + "description": "`clientId` of the providing client (matches `initialize`)." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "SessionCanvasAction": { + "type": "object", + "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "properties": { + "name": { + "type": "string", + "description": "Action name, unique within the owning `(extensionId, canvasId)`." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the action does." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + } + }, + "required": [ + "name" + ] + }, + "SessionCanvasDeclaration": { + "type": "object", + "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", + "properties": { + "extensionId": { + "type": "string", + "description": "Owning provider id. Stable across declarations and instances." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id. Unique within `extensionId`." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + }, + "source": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Where the declaration came from — for routing and cleanup." + } + }, + "required": [ + "extensionId", + "canvasId", + "displayName", + "description", + "source" + ] + }, + "ClientCanvasDeclaration": { + "type": "object", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "properties": { + "canvasId": { + "type": "string", + "description": "Provider-local canvas id, unique within the publishing client." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + } + }, + "required": [ + "canvasId", + "displayName", + "description" + ] + }, + "OpenCanvasRef": { + "type": "object", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "title": { + "type": "string", + "description": "Current instance title, mirrored from {@link CanvasState.title}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether the instance's provider is currently available." + } + }, + "required": [ + "instanceId", + "channel", + "canvasId", + "extensionId", + "availability" + ] + }, "SessionInputRequestBase": { "type": "object", "description": "Fields common to every {@link SessionInputRequest} variant.", @@ -4412,6 +4634,64 @@ "type" ] }, + "CanvasState": { + "type": "object", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Input the agent supplied when opening the instance. Retained so the\ninstance can be resumed or rebound after a reconnect." + }, + "title": { + "type": "string", + "description": "Current instance title." + }, + "status": { + "type": "string", + "description": "Provider-defined status string (opaque to AHP)." + }, + "url": { + "type": "string", + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether this instance's provider is currently available." + }, + "provider": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Which provider owns the callbacks (`canvasOpen` / … ) for this instance." + } + }, + "required": [ + "instanceId", + "canvasId", + "extensionId", + "availability", + "provider" + ] + }, "BaseParams": { "type": "object", "description": "Base shape every command's params extends.\n\n`channel` identifies the channel the command targets, mirroring the\n`channel` field on every protocol notification. For commands that operate\non a specific channel (a session, terminal, or changeset), `channel` is\nthat channel's URI. For commands that are connection-level rather than\nchannel-scoped (e.g. {@link InitializeParams | `initialize`},\n{@link PingParams | `ping`}, {@link ListSessionsParams | `listSessions`},\nthe `resource*` filesystem commands, and {@link AuthenticateParams |\n`authenticate`}), the params type narrows `channel` to the literal\nroot URI `'ahp-root://'`.\n\nThis invariant lets implementations route every incoming message —\nrequest, response, or notification — by inspecting `params.channel`\nwithout needing to know the per-method param shape.", @@ -4500,6 +4780,11 @@ "type": "object", "additionalProperties": {}, "description": "Client can render\n[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.\nit can host the View sandbox, run the `ui/*` protocol against it,\nand forward `mcp://`-channel traffic on the App's behalf.\n\nHosts SHOULD only populate\n{@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}\n(and expose the corresponding\n{@link McpServerCustomization.channel | `mcp://` channel}) when this\ncapability is declared. Clients that omit it MUST treat\nApp-bearing tool calls as ordinary MCP tool calls." + }, + "canvas": { + "type": "object", + "additionalProperties": {}, + "description": "Client can render canvases and host client-declared canvas providers — it\ncan render an opaque canvas URL in an isolated surface, and it can answer\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases\nit declares via {@link SessionActiveClient.canvasProviders}.\n\nHosts SHOULD only populate {@link SessionState.canvases} /\n{@link SessionState.openCanvases} and only route canvas requests to a\nclient that declared this capability. Clients that omit it see no canvas\nsurface. See {@link /specification/canvas-channel | Canvas Channel}." } } }, @@ -5682,6 +5967,191 @@ "required": [ "channel" ] + }, + "CanvasOpenParams": { + "type": "object", + "description": "Opens a canvas instance against its provider.\n\nSent by the host to the client that declared the target canvas via\n{@link SessionActiveClient.canvasProviders} (a client that also declared\n{@link ClientCapabilities.canvas}). For a server-side provider the host\nresolves the open host-internally and emits no request. The provider returns\nthe initial render target and presentation fields, which the host folds into\nthe new instance's {@link CanvasState}.\n\nMirrors the `resource*` precedent: registered in `ServerCommandMap` and\nmirrored in `CommandMap` for symmetry. A client normally never initiates it —\nthe host is not a canvas provider — and a receiver SHOULD reject a request\nwhose target is not one of its declared providers.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning session channel URI (`ahp-session:/`)." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id to open." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "instanceId": { + "type": "string", + "description": "Caller-minted handle for the new instance." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Open input, validated by the provider against its declared schema." + } + }, + "required": [ + "channel", + "canvasId", + "extensionId", + "instanceId" + ] + }, + "CanvasOpenResult": { + "type": "object", + "description": "Result of the `canvasOpen` command.", + "properties": { + "url": { + "type": "string", + "description": "Initial content address for the instance (see {@link CanvasState.url})." + }, + "title": { + "type": "string", + "description": "Initial title." + }, + "status": { + "type": "string", + "description": "Initial provider-defined status." + } + } + }, + "CanvasInvokeActionParams": { + "type": "object", + "description": "Invokes one of a canvas's declared actions against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) when the agent invokes a\n{@link SessionCanvasAction | declared action} on an open instance. The\nprovider returns an opaque, provider-defined value. Registered symmetrically\nwith the rest of the provider family (see {@link CanvasOpenParams}).", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning session channel URI (`ahp-session:/`)." + }, + "instanceId": { + "type": "string", + "description": "Instance handle the action targets." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id of the instance." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "actionName": { + "type": "string", + "description": "Declared action name to invoke." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Action input, validated by the provider against its declared schema." + } + }, + "required": [ + "channel", + "instanceId", + "canvasId", + "extensionId", + "actionName" + ] + }, + "CanvasInvokeActionResult": { + "type": "object", + "description": "Result of the `canvasInvokeAction` command.", + "properties": { + "value": { + "description": "Opaque, provider-defined return value." + } + } + }, + "CanvasCloseParams": { + "type": "object", + "description": "Closes a canvas instance against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) as part of the close flow — typically after a client\ndispatches `canvas/closeRequested`. The host then drops the instance from\n{@link SessionState.openCanvases}. Registered symmetrically with the rest of\nthe provider family (see {@link CanvasOpenParams}).", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning session channel URI (`ahp-session:/`)." + }, + "instanceId": { + "type": "string", + "description": "Instance handle to close." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id of the instance." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + } + }, + "required": [ + "channel", + "instanceId", + "canvasId", + "extensionId" + ] + }, + "CanvasReadResourceParams": { + "type": "object", + "description": "Reads channel-served canvas content by `ahp-canvas-content:` URI.\n\nA client → host request, modeled on MCP's `resources/read`. When a canvas's\n{@link CanvasState.url} (or a sub-resource the rendered document references)\nis an `ahp-canvas-content://` address, the renderer cannot\ndial the host directly — for example in a relayed deployment behind a broker\n— so it resolves the bytes over the instance's existing `ahp-canvas:/`\nchannel with this request instead of loading a network URL. The ``\nsegment of the URI identifies which canvas channel to read from. See\n{@link /specification/canvas-channel | Canvas Channel}.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "The owning canvas channel URI (`ahp-canvas:/`)." + }, + "uri": { + "type": "string", + "description": "An `ahp-canvas-content://` content URI to read." + } + }, + "required": [ + "channel", + "uri" + ] + }, + "CanvasReadResourceResult": { + "type": "object", + "description": "Result of the `canvasReadResource` command.", + "properties": { + "contents": { + "type": "array", + "items": { + "$ref": "#/$defs/CanvasResourceContent" + }, + "description": "The resolved content parts, wrapped for forward compatibility." + } + }, + "required": [ + "contents" + ] + }, + "CanvasResourceContent": { + "type": "object", + "description": "One resolved piece of channel-served canvas content.\n\nCarries exactly one of {@link text} (text payloads) or {@link blob}\n(base64-encoded binary payloads).", + "properties": { + "uri": { + "type": "string", + "description": "The content URI this part resolves." + }, + "mimeType": { + "type": "string", + "description": "MIME type of the content, when known." + }, + "text": { + "type": "string", + "description": "UTF-8 text content, for text payloads." + }, + "blob": { + "type": "string", + "description": "Base64-encoded content, for binary payloads." + } + }, + "required": [ + "uri" + ] } } } diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index fe8b92f3..bddc77e1 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -672,6 +672,9 @@ { "$ref": "#/$defs/ResourceWatchState" }, + { + "$ref": "#/$defs/CanvasState" + }, { "$ref": "#/$defs/AnnotationsState" }, @@ -1015,6 +1018,20 @@ }, "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Aggregated canvas registry currently exposed to the agent — the union of\nevery connected provider (server-side and client-declared). Each entry\ndescribes a canvas the agent can open; the host folds\n{@link SessionActiveClient.canvasProviders | client-declared providers}\ninto this list alongside its own server-side providers.\n\nFull-replacement via `session/canvasesChanged`. Populated only when at\nleast one connected client declared {@link ClientCapabilities.canvas};\nabsent for sessions with no canvas surface. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Lightweight catalogue of currently-open canvas instances. Each entry\ncarries the instance's `ahp-canvas:/` channel URI so a subscriber can\nsubscribe to the full {@link CanvasState} and render it — analogous to how\n{@link RootState.terminals} catalogues live terminals whose full state\nlives on each terminal channel.\n\nFull-replacement via `session/openCanvasesChanged`." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1055,6 +1072,13 @@ "$ref": "#/$defs/ClientPluginCustomization" }, "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + }, + "canvasProviders": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientCanvasDeclaration" + }, + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -1062,6 +1086,182 @@ "tools" ] }, + "CanvasServerProviderSource": { + "type": "object", + "description": "A canvas provided by the host. Carries no `clientId` — server-side provider\nrequests are resolved host-internally rather than routed to a peer.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Server" + } + }, + "required": [ + "kind" + ] + }, + "CanvasClientProviderSource": { + "type": "object", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Client" + }, + "clientId": { + "type": "string", + "description": "`clientId` of the providing client (matches `initialize`)." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "SessionCanvasAction": { + "type": "object", + "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "properties": { + "name": { + "type": "string", + "description": "Action name, unique within the owning `(extensionId, canvasId)`." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the action does." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + } + }, + "required": [ + "name" + ] + }, + "SessionCanvasDeclaration": { + "type": "object", + "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", + "properties": { + "extensionId": { + "type": "string", + "description": "Owning provider id. Stable across declarations and instances." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id. Unique within `extensionId`." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + }, + "source": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Where the declaration came from — for routing and cleanup." + } + }, + "required": [ + "extensionId", + "canvasId", + "displayName", + "description", + "source" + ] + }, + "ClientCanvasDeclaration": { + "type": "object", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "properties": { + "canvasId": { + "type": "string", + "description": "Provider-local canvas id, unique within the publishing client." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + } + }, + "required": [ + "canvasId", + "displayName", + "description" + ] + }, + "OpenCanvasRef": { + "type": "object", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "title": { + "type": "string", + "description": "Current instance title, mirrored from {@link CanvasState.title}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether the instance's provider is currently available." + } + }, + "required": [ + "instanceId", + "channel", + "canvasId", + "extensionId", + "availability" + ] + }, "SessionInputRequestBase": { "type": "object", "description": "Fields common to every {@link SessionInputRequest} variant.", @@ -4571,6 +4771,64 @@ "uri", "type" ] + }, + "CanvasState": { + "type": "object", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Input the agent supplied when opening the instance. Retained so the\ninstance can be resumed or rebound after a reconnect." + }, + "title": { + "type": "string", + "description": "Current instance title." + }, + "status": { + "type": "string", + "description": "Provider-defined status string (opaque to AHP)." + }, + "url": { + "type": "string", + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether this instance's provider is currently available." + }, + "provider": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Which provider owns the callbacks (`canvasOpen` / … ) for this instance." + } + }, + "required": [ + "instanceId", + "canvasId", + "extensionId", + "availability", + "provider" + ] } } } diff --git a/schema/state.schema.json b/schema/state.schema.json index f58baac1..2bcbce01 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -423,6 +423,9 @@ { "$ref": "#/$defs/ResourceWatchState" }, + { + "$ref": "#/$defs/CanvasState" + }, { "$ref": "#/$defs/AnnotationsState" }, @@ -766,6 +769,20 @@ }, "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." }, + "canvases": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasDeclaration" + }, + "description": "Aggregated canvas registry currently exposed to the agent — the union of\nevery connected provider (server-side and client-declared). Each entry\ndescribes a canvas the agent can open; the host folds\n{@link SessionActiveClient.canvasProviders | client-declared providers}\ninto this list alongside its own server-side providers.\n\nFull-replacement via `session/canvasesChanged`. Populated only when at\nleast one connected client declared {@link ClientCapabilities.canvas};\nabsent for sessions with no canvas surface. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "openCanvases": { + "type": "array", + "items": { + "$ref": "#/$defs/OpenCanvasRef" + }, + "description": "Lightweight catalogue of currently-open canvas instances. Each entry\ncarries the instance's `ahp-canvas:/` channel URI so a subscriber can\nsubscribe to the full {@link CanvasState} and render it — analogous to how\n{@link RootState.terminals} catalogues live terminals whose full state\nlives on each terminal channel.\n\nFull-replacement via `session/openCanvasesChanged`." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -806,6 +823,13 @@ "$ref": "#/$defs/ClientPluginCustomization" }, "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + }, + "canvasProviders": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientCanvasDeclaration" + }, + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -813,6 +837,182 @@ "tools" ] }, + "CanvasServerProviderSource": { + "type": "object", + "description": "A canvas provided by the host. Carries no `clientId` — server-side provider\nrequests are resolved host-internally rather than routed to a peer.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Server" + } + }, + "required": [ + "kind" + ] + }, + "CanvasClientProviderSource": { + "type": "object", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "properties": { + "kind": { + "$ref": "#/$defs/CanvasProviderKind.Client" + }, + "clientId": { + "type": "string", + "description": "`clientId` of the providing client (matches `initialize`)." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "SessionCanvasAction": { + "type": "object", + "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "properties": { + "name": { + "type": "string", + "description": "Action name, unique within the owning `(extensionId, canvasId)`." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the action does." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + } + }, + "required": [ + "name" + ] + }, + "SessionCanvasDeclaration": { + "type": "object", + "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", + "properties": { + "extensionId": { + "type": "string", + "description": "Owning provider id. Stable across declarations and instances." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id. Unique within `extensionId`." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + }, + "source": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Where the declaration came from — for routing and cleanup." + } + }, + "required": [ + "extensionId", + "canvasId", + "displayName", + "description", + "source" + ] + }, + "ClientCanvasDeclaration": { + "type": "object", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "properties": { + "canvasId": { + "type": "string", + "description": "Provider-local canvas id, unique within the publishing client." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "description": { + "type": "string", + "description": "Human-readable description of the canvas." + }, + "inputSchema": { + "type": "object", + "additionalProperties": {}, + "description": "JSON Schema for the canvas's open input. Opaque to AHP." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionCanvasAction" + }, + "description": "Actions this canvas exposes to the agent." + } + }, + "required": [ + "canvasId", + "displayName", + "description" + ] + }, + "OpenCanvasRef": { + "type": "object", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "title": { + "type": "string", + "description": "Current instance title, mirrored from {@link CanvasState.title}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether the instance's provider is currently available." + } + }, + "required": [ + "instanceId", + "channel", + "canvasId", + "extensionId", + "availability" + ] + }, "SessionInputRequestBase": { "type": "object", "description": "Fields common to every {@link SessionInputRequest} variant.", @@ -4323,6 +4523,64 @@ "type" ] }, + "CanvasState": { + "type": "object", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "properties": { + "instanceId": { + "type": "string", + "description": "Server-assigned instance handle, unique within the session." + }, + "canvasId": { + "type": "string", + "description": "Provider-local canvas id this instance was opened from." + }, + "extensionId": { + "type": "string", + "description": "Owning provider id." + }, + "extensionName": { + "type": "string", + "description": "Human-readable provider name." + }, + "displayName": { + "type": "string", + "description": "Human-readable canvas name." + }, + "input": { + "type": "object", + "additionalProperties": {}, + "description": "Input the agent supplied when opening the instance. Retained so the\ninstance can be resumed or rebound after a reconnect." + }, + "title": { + "type": "string", + "description": "Current instance title." + }, + "status": { + "type": "string", + "description": "Provider-defined status string (opaque to AHP)." + }, + "url": { + "type": "string", + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + }, + "availability": { + "$ref": "#/$defs/CanvasAvailability", + "description": "Whether this instance's provider is currently available." + }, + "provider": { + "$ref": "#/$defs/CanvasProviderSource", + "description": "Which provider owns the callbacks (`canvasOpen` / … ) for this instance." + } + }, + "required": [ + "instanceId", + "canvasId", + "extensionId", + "availability", + "provider" + ] + }, "StringOrMarkdown": { "oneOf": [ { @@ -4357,6 +4615,18 @@ ], "description": "A primitive JSON value: a string, number, boolean, or `null`." }, + "CanvasProviderSource": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/CanvasServerProviderSource" + }, + { + "$ref": "#/$defs/CanvasClientProviderSource" + } + ], + "description": "Where a canvas declaration came from — used for request routing and for\ncleaning the entry up when its provider disconnects. Modeled as a\ndiscriminated union so the `clientId` is only present (and required) for the\nclient variant." + }, "SessionInputRequest": { "oneOf": [ {}, diff --git a/scripts/find-protocol-sources.ts b/scripts/find-protocol-sources.ts index 862088bd..c1b3d8e7 100644 --- a/scripts/find-protocol-sources.ts +++ b/scripts/find-protocol-sources.ts @@ -24,6 +24,7 @@ export const PROTOCOL_SOURCE_DIRS: readonly string[] = [ 'channels-annotations', 'channels-otlp', 'channels-resource-watch', + 'channels-canvas', ]; /** diff --git a/scripts/generate-action-origin.ts b/scripts/generate-action-origin.ts index 0b66aa73..395767f7 100644 --- a/scripts/generate-action-origin.ts +++ b/scripts/generate-action-origin.ts @@ -17,7 +17,7 @@ const GENERATED_HEADER = `// Generated from types/actions.ts — do not edit // Run \`npm run generate\` to regenerate. `; -type ActionScope = 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; +type ActionScope = 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch' | 'canvas'; interface ActionInfo { /** The interface name (e.g. 'RootAgentsChangedAction') */ @@ -155,6 +155,7 @@ export function generateActionOrigin(project: Project, outDir: string): void { : category === 'Changeset Actions' ? 'changeset' : category === 'Annotations Actions' ? 'annotations' : category === 'Resource Watch Actions' ? 'resourceWatch' + : category === 'Canvas Actions' ? 'canvas' : 'session'; const isClientDispatchable = hasJsDocTag(node as any, 'clientDispatchable'); @@ -207,6 +208,7 @@ export function generateActionOrigin(project: Project, outDir: string): void { const changesetActions = actions.filter(a => a.scope === 'changeset'); const annotationsActions = actions.filter(a => a.scope === 'annotations'); const resourceWatchActions = actions.filter(a => a.scope === 'resourceWatch'); + const canvasActions = actions.filter(a => a.scope === 'canvas'); const clientRootActions = rootActions.filter(a => a.isClientDispatchable); const serverRootActions = rootActions.filter(a => !a.isClientDispatchable); const clientSessionActions = sessionActions.filter(a => a.isClientDispatchable); @@ -221,6 +223,8 @@ export function generateActionOrigin(project: Project, outDir: string): void { const serverAnnotationsActions = annotationsActions.filter(a => !a.isClientDispatchable); const clientResourceWatchActions = resourceWatchActions.filter(a => a.isClientDispatchable); const serverResourceWatchActions = resourceWatchActions.filter(a => !a.isClientDispatchable); + const clientCanvasActions = canvasActions.filter(a => a.isClientDispatchable); + const serverCanvasActions = canvasActions.filter(a => !a.isClientDispatchable); const lines: string[] = [GENERATED_HEADER]; @@ -459,6 +463,46 @@ export function generateActionOrigin(project: Project, outDir: string): void { lines.push(``); + // CanvasAction + lines.push(`/** Union of all canvas-scoped actions. */`); + lines.push(`export type CanvasAction =`); + if (canvasActions.length === 0) { + lines.push(` never`); + } else { + for (let i = 0; i < canvasActions.length; i++) { + lines.push(` | ${canvasActions[i].name}`); + } + } + lines.push(`;`); + lines.push(``); + + // ClientCanvasAction + lines.push(`/** Union of canvas actions that clients may dispatch. */`); + lines.push(`export type ClientCanvasAction =`); + if (clientCanvasActions.length === 0) { + lines.push(` never`); + } else { + for (let i = 0; i < clientCanvasActions.length; i++) { + lines.push(` | ${clientCanvasActions[i].name}`); + } + } + lines.push(`;`); + lines.push(``); + + // ServerCanvasAction + lines.push(`/** Union of canvas actions that only the server may produce. */`); + lines.push(`export type ServerCanvasAction =`); + if (serverCanvasActions.length === 0) { + lines.push(` never`); + } else { + for (let i = 0; i < serverCanvasActions.length; i++) { + lines.push(` | ${serverCanvasActions[i].name}`); + } + } + lines.push(`;`); + lines.push(``); + + // IS_CLIENT_DISPATCHABLE map lines.push(`// ─── Client-Dispatchable Map ─────────────────────────────────────────────────`); lines.push(``); diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index e3e9627e..d38d9243 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -171,6 +171,7 @@ function mapType(tsType: string): string { tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | CanvasState | AnnotationsState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState' ) { @@ -649,6 +650,7 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'CanvasAvailability', 'CanvasProviderKind', ]; const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ @@ -768,6 +770,13 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, + { name: 'SessionCanvasAction' }, + { name: 'SessionCanvasDeclaration' }, + { name: 'ClientCanvasDeclaration' }, + { name: 'OpenCanvasRef' }, + { name: 'CanvasServerProviderSource' }, + { name: 'CanvasClientProviderSource' }, + { name: 'CanvasState' }, ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -978,6 +987,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const CANVAS_PROVIDER_SOURCE_UNION: UnionConfig = { + name: 'CanvasProviderSource', + discriminantField: 'kind', + doc: 'CanvasProviderSource identifies where a canvas declaration came from — used for request routing and cleanup when its provider disconnects.', + variants: [ + { variantName: 'Server', innerType: 'CanvasServerProviderSource', wireValue: 'server' }, + { variantName: 'Client', innerType: 'CanvasClientProviderSource', wireValue: 'client' }, + ], + unknown: true, +}; + function generateChatOriginGo(): string { return `// ChatOrigin describes how a chat came into existence. type ChatOrigin struct { @@ -1063,10 +1083,10 @@ func (o ChatOrigin) MarshalJSON() ([]byte, error) { function generateSnapshotState(): string { return `// SnapshotState is the state payload of a snapshot — root, session, -// chat, terminal, changeset, resource-watch, or annotations state. The active -// variant is chosen by which pointer field is non-nil; UnmarshalJSON probes -// for required fields in the canonical order -// (session → chat → terminal → changeset → resourceWatch → annotations → root). +// chat, terminal, changeset, resource-watch, canvas, or annotations state. The +// active variant is chosen by which pointer field is non-nil; UnmarshalJSON +// probes for required fields in the canonical order +// (session → chat → terminal → changeset → resourceWatch → canvas → annotations → root). type SnapshotState struct { \tRoot *RootState \`json:"-"\` \tSession *SessionState \`json:"-"\` @@ -1074,6 +1094,7 @@ type SnapshotState struct { \tTerminal *TerminalState \`json:"-"\` \tChangeset *ChangesetState \`json:"-"\` \tResourceWatch *ResourceWatchState \`json:"-"\` +\tCanvas *CanvasState \`json:"-"\` \tAnnotations *AnnotationsState \`json:"-"\` } @@ -1090,6 +1111,8 @@ func (s SnapshotState) MarshalJSON() ([]byte, error) { \t\treturn json.Marshal(s.Changeset) \tcase s.ResourceWatch != nil: \t\treturn json.Marshal(s.ResourceWatch) +\tcase s.Canvas != nil: +\t\treturn json.Marshal(s.Canvas) \tcase s.Annotations != nil: \t\treturn json.Marshal(s.Annotations) \tcase s.Root != nil: @@ -1138,6 +1161,12 @@ func (s *SnapshotState) UnmarshalJSON(data []byte) error { \t\t\treturn err \t\t} \t\ts.ResourceWatch = &v +\tcase containsAll(probe, "canvasId", "provider"): +\t\tvar v CanvasState +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\ts.Canvas = &v \tcase containsAll(probe, "annotations"): \t\tvar v AnnotationsState \t\tif err := json.Unmarshal(data, &v); err != nil { @@ -1223,6 +1252,7 @@ function generateStateFile(project: Project): string { lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); + lines.push(generateDiscriminatedUnion(CANVAS_PROVIDER_SOURCE_UNION)); lines.push(''); lines.push(generateChatOriginGo()); lines.push(''); @@ -1278,6 +1308,8 @@ const ACTION_VARIANTS: { { type: 'session/activityChanged', variantName: 'SessionActivityChanged', tsInterface: 'SessionActivityChangedAction' }, { type: 'session/changesetsChanged', variantName: 'SessionChangesetsChanged', tsInterface: 'SessionChangesetsChangedAction' }, { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, + { type: 'session/canvasesChanged', variantName: 'SessionCanvasesChanged', tsInterface: 'SessionCanvasesChangedAction' }, + { type: 'session/openCanvasesChanged', variantName: 'SessionOpenCanvasesChanged', tsInterface: 'SessionOpenCanvasesChangedAction' }, { type: 'session/activeClientSet', variantName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', variantName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, @@ -1314,6 +1346,9 @@ const ACTION_VARIANTS: { { type: 'terminal/commandExecuted', variantName: 'TerminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', variantName: 'TerminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'canvas/updated', variantName: 'CanvasUpdated', tsInterface: 'CanvasUpdatedAction' }, + { type: 'canvas/closeRequested', variantName: 'CanvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, + { type: 'canvas/message', variantName: 'CanvasMessage', tsInterface: 'CanvasMessageAction' }, ]; function generateMergedChatToolCallConfirmedStruct(): string { @@ -1449,6 +1484,11 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'CompletionsParams' }, { name: 'CompletionItem' }, { name: 'CompletionsResult' }, { name: 'InvokeChangesetOperationParams' }, { name: 'InvokeChangesetOperationResult' }, { name: 'ChangesetOperationFollowUp' }, + { name: 'CanvasOpenParams' }, { name: 'CanvasOpenResult' }, + { name: 'CanvasInvokeActionParams' }, { name: 'CanvasInvokeActionResult' }, + { name: 'CanvasCloseParams' }, + { name: 'CanvasReadResourceParams' }, { name: 'CanvasReadResourceResult' }, + { name: 'CanvasResourceContent' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1662,6 +1702,8 @@ const ( \tErrorCodeNotFound int32 = -32008 \tErrorCodePermissionDenied int32 = -32009 \tErrorCodeAlreadyExists int32 = -32010 +\tErrorCodeConflict int32 = -32011 +\tErrorCodeCanvasProviderError int32 = -32012 ) // AhpErrorCode is the type alias used by AHP application error codes. @@ -1689,6 +1731,14 @@ type PermissionDeniedErrorData struct { type UnsupportedProtocolVersionErrorData struct { \tSupportedVersions []string \`json:"supportedVersions"\` } + +// CanvasProviderErrorData is the detail payload of a +// CanvasProviderError (-32012) error. The Code is a provider-defined +// string (opaque to AHP) identifying the failure. +type CanvasProviderErrorData struct { +\tCode string \`json:"code"\` +\tMessage string \`json:"message"\` +} `; } @@ -1923,11 +1973,13 @@ function checkExhaustiveness(project: Project): void { 'McpServerState', 'ToolCallContributor', 'SessionInputRequest', + 'CanvasProviderSource', 'ToolCallConfirmationState', 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', + 'CanvasProviderErrorData', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 01a9fe46..f95fc6a6 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -147,6 +147,7 @@ function mapType(tsType: string): string { tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | CanvasState | AnnotationsState | ChatState' || tsType === 'RootState | SessionState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' || @@ -660,7 +661,7 @@ internal object StringOrMarkdownSerializer : KSerializer { function generateSnapshotState(): string { return `/** * The state payload of a snapshot — root, session, chat, terminal, changeset, - * resource-watch, or annotations state. + * resource-watch, canvas, or annotations state. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @@ -670,6 +671,7 @@ sealed interface SnapshotState { @JvmInline value class Terminal(val value: TerminalState) : SnapshotState @JvmInline value class Changeset(val value: ChangesetState) : SnapshotState @JvmInline value class ResourceWatch(val value: ResourceWatchState) : SnapshotState + @JvmInline value class Canvas(val value: CanvasState) : SnapshotState @JvmInline value class Annotations(val value: AnnotationsState) : SnapshotState } @@ -686,7 +688,8 @@ internal object SnapshotStateSerializer : KSerializer { // Try the most distinctive shape first. SessionState has required // \`lifecycle\`; ChatState has required \`turns\`; ChangesetState has // required \`status\` + \`files\`; ResourceWatchState has required - // \`root\` + \`recursive\`; AnnotationsState has required \`annotations\` + // \`root\` + \`recursive\`; CanvasState has required \`canvasId\` + + // \`provider\`; AnnotationsState has required \`annotations\` // (checked after session, whose optional annotations summary reuses the // key); TerminalState has required \`content\`; RootState is the // catch-all. @@ -697,6 +700,8 @@ internal object SnapshotStateSerializer : KSerializer { SnapshotState.Changeset(input.json.decodeFromJsonElement(ChangesetState.serializer(), element)) obj.containsKey("root") && obj.containsKey("recursive") -> SnapshotState.ResourceWatch(input.json.decodeFromJsonElement(ResourceWatchState.serializer(), element)) + obj.containsKey("canvasId") && obj.containsKey("provider") -> + SnapshotState.Canvas(input.json.decodeFromJsonElement(CanvasState.serializer(), element)) obj.containsKey("annotations") -> SnapshotState.Annotations(input.json.decodeFromJsonElement(AnnotationsState.serializer(), element)) obj.containsKey("content") -> @@ -715,6 +720,7 @@ internal object SnapshotStateSerializer : KSerializer { is SnapshotState.Terminal -> output.json.encodeToJsonElement(TerminalState.serializer(), value.value) is SnapshotState.Changeset -> output.json.encodeToJsonElement(ChangesetState.serializer(), value.value) is SnapshotState.ResourceWatch -> output.json.encodeToJsonElement(ResourceWatchState.serializer(), value.value) + is SnapshotState.Canvas -> output.json.encodeToJsonElement(CanvasState.serializer(), value.value) is SnapshotState.Annotations -> output.json.encodeToJsonElement(AnnotationsState.serializer(), value.value) } output.encodeJsonElement(element) @@ -793,6 +799,7 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'CanvasAvailability', 'CanvasProviderKind', ]; const STATE_STRUCTS = [ @@ -843,6 +850,8 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', + 'SessionCanvasAction', 'SessionCanvasDeclaration', 'ClientCanvasDeclaration', 'OpenCanvasRef', + 'CanvasServerProviderSource', 'CanvasClientProviderSource', 'CanvasState', ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -1081,6 +1090,16 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const CANVAS_PROVIDER_SOURCE_UNION: UnionConfig = { + name: 'CanvasProviderSource', + discriminantField: 'kind', + variants: [ + { caseName: 'Server', structName: 'CanvasServerProviderSource', discriminantValue: 'server' }, + { caseName: 'Client', structName: 'CanvasClientProviderSource', discriminantValue: 'client' }, + ], + unknown: true, +}; + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -1150,6 +1169,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(CANVAS_PROVIDER_SOURCE_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1191,6 +1212,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/activityChanged', caseName: 'SessionActivityChanged', tsInterface: 'SessionActivityChangedAction' }, { type: 'session/changesetsChanged', caseName: 'SessionChangesetsChanged', tsInterface: 'SessionChangesetsChangedAction' }, { type: 'session/serverToolsChanged', caseName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, + { type: 'session/canvasesChanged', caseName: 'SessionCanvasesChanged', tsInterface: 'SessionCanvasesChangedAction' }, + { type: 'session/openCanvasesChanged', caseName: 'SessionOpenCanvasesChanged', tsInterface: 'SessionOpenCanvasesChangedAction' }, { type: 'session/activeClientSet', caseName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', caseName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, { type: 'session/inputNeededSet', caseName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, @@ -1236,6 +1259,9 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'terminal/commandExecuted', caseName: 'TerminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', caseName: 'TerminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', caseName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'canvas/updated', caseName: 'CanvasUpdated', tsInterface: 'CanvasUpdatedAction' }, + { type: 'canvas/closeRequested', caseName: 'CanvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, + { type: 'canvas/message', caseName: 'CanvasMessage', tsInterface: 'CanvasMessageAction' }, ]; /** Merged data class for the approved/denied tool call confirmed action. */ @@ -1423,6 +1449,10 @@ const COMMAND_STRUCTS = [ 'CompletionsParams', 'CompletionItem', 'CompletionsResult', 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', + 'CanvasOpenParams', 'CanvasOpenResult', + 'CanvasInvokeActionParams', 'CanvasInvokeActionResult', + 'CanvasCloseParams', + 'CanvasReadResourceParams', 'CanvasReadResourceResult', 'CanvasResourceContent', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1659,12 +1689,16 @@ function generateErrorsFile(project: Project): string { lines.push(' const val PERMISSION_DENIED: Int = -32009'); lines.push(' /** The target resource already exists and the operation does not allow overwriting */'); lines.push(' const val ALREADY_EXISTS: Int = -32010'); + lines.push(' /** An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state */'); + lines.push(' const val CONFLICT: Int = -32011'); + lines.push(' /** A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` */'); + lines.push(' const val CANVAS_PROVIDER_ERROR: Int = -32012'); lines.push('}'); lines.push(''); lines.push('// ─── Error Detail Payloads ──────────────────────────────────────────────────'); lines.push(''); - for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData']) { + for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'CanvasProviderErrorData']) { try { lines.push(generateDataClassFromInterface(project, ifaceName)); lines.push(''); @@ -1910,12 +1944,14 @@ function checkExhaustiveness(project: Project): void { 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'CanvasProviderSource', // CANVAS_PROVIDER_SOURCE_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; consumers reuse CustomizationType 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() + 'CanvasProviderErrorData', // emitted by generateErrorsFile() 'AhpError', // typed via JsonRpcError; not a Kotlin data class 'AhpErrorDetailsMap', // type-level mapping; not a Kotlin type 'AhpErrorCode', // type-level alias over AhpErrorCodes const enum diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 3d408b9a..a2004557 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -158,6 +158,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' + || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | CanvasState | AnnotationsState | ChatState' || tsType === 'RootState | SessionState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' @@ -659,6 +660,7 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'CanvasAvailability', 'CanvasProviderKind', ]; /** @@ -799,6 +801,13 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, + { name: 'SessionCanvasAction' }, + { name: 'SessionCanvasDeclaration' }, + { name: 'ClientCanvasDeclaration' }, + { name: 'OpenCanvasRef' }, + { name: 'CanvasServerProviderSource', omitDiscriminants: true }, + { name: 'CanvasClientProviderSource', omitDiscriminants: true }, + { name: 'CanvasState' }, ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -1014,6 +1023,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const CANVAS_PROVIDER_SOURCE_UNION: UnionConfig = { + name: 'CanvasProviderSource', + discriminantField: 'kind', + doc: 'Where a canvas declaration came from — used for request routing and cleanup when its provider disconnects.', + variants: [ + { variantName: 'Server', innerType: 'CanvasServerProviderSource', wireValue: 'server' }, + { variantName: 'Client', innerType: 'CanvasClientProviderSource', wireValue: 'client' }, + ], + unknown: true, +}; + function generateChatOrigin(): string { return `/// How a chat came into existence. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -1049,12 +1069,13 @@ pub enum ChatOrigin { function generateSnapshotState(): string { return `/// The state payload of a snapshot — root, session, chat, terminal, -/// changeset, resource-watch, or annotations state. +/// changeset, resource-watch, canvas, or annotations state. /// /// Deserialized by trying session first (has required \`lifecycle\`), then /// chat (has required \`turns\`), then terminal (has required \`content\`), /// then changeset (has required \`status\` and \`files\`), then resource-watch -/// (has required \`root\` and \`recursive\`), then annotations (has required +/// (has required \`root\` and \`recursive\`), then canvas (has required +/// \`canvasId\` and \`provider\`), then annotations (has required /// \`annotations\`), then root. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] @@ -1064,6 +1085,7 @@ pub enum SnapshotState { Terminal(Box), Changeset(Box), ResourceWatch(Box), + Canvas(Box), Annotations(Box), Root(Box), }`; @@ -1136,6 +1158,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(CANVAS_PROVIDER_SOURCE_UNION)); + lines.push(''); lines.push(generateSnapshotState()); lines.push(''); @@ -1183,6 +1207,8 @@ const ACTION_VARIANTS: { { type: 'session/activityChanged', variantName: 'SessionActivityChanged', tsInterface: 'SessionActivityChangedAction' }, { type: 'session/changesetsChanged', variantName: 'SessionChangesetsChanged', tsInterface: 'SessionChangesetsChangedAction' }, { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, + { type: 'session/canvasesChanged', variantName: 'SessionCanvasesChanged', tsInterface: 'SessionCanvasesChangedAction' }, + { type: 'session/openCanvasesChanged', variantName: 'SessionOpenCanvasesChanged', tsInterface: 'SessionOpenCanvasesChangedAction' }, { type: 'session/activeClientSet', variantName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', variantName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction', boxed: true }, @@ -1227,6 +1253,9 @@ const ACTION_VARIANTS: { { type: 'terminal/commandExecuted', variantName: 'TerminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', variantName: 'TerminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'canvas/updated', variantName: 'CanvasUpdated', tsInterface: 'CanvasUpdatedAction' }, + { type: 'canvas/closeRequested', variantName: 'CanvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, + { type: 'canvas/message', variantName: 'CanvasMessage', tsInterface: 'CanvasMessageAction' }, ]; function generateMergedToolCallConfirmedStruct(scope: 'Session' | 'Chat' = 'Session'): string { @@ -1265,7 +1294,7 @@ pub struct ${scope}ToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary, CanvasAvailability, OpenCanvasRef, SessionCanvasDeclaration};'); lines.push(''); // ActionType enum @@ -1399,6 +1428,11 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'CompletionsParams' }, { name: 'CompletionItem' }, { name: 'CompletionsResult' }, { name: 'InvokeChangesetOperationParams' }, { name: 'InvokeChangesetOperationResult' }, { name: 'ChangesetOperationFollowUp' }, + { name: 'CanvasOpenParams' }, { name: 'CanvasOpenResult' }, + { name: 'CanvasInvokeActionParams' }, { name: 'CanvasInvokeActionResult' }, + { name: 'CanvasCloseParams' }, + { name: 'CanvasReadResourceParams' }, { name: 'CanvasReadResourceResult' }, + { name: 'CanvasResourceContent' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1610,6 +1644,8 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. \`ResourceWriteParams.if_match\`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; + /// A canvas provider request (\`canvasOpen\`, \`canvasInvokeAction\`, or \`canvasClose\`) failed; the error \`data\` carries a provider-defined \`{ code, message }\`. + pub const CANVAS_PROVIDER_ERROR: i32 = -32012; } /// Type alias: AHP application error code. @@ -1648,6 +1684,17 @@ pub struct UnsupportedProtocolVersionErrorData { /// SemVer range constraint (e.g. \`">=0.1.0 <0.3.0"\` or \`"^0.2.0"\`). pub supported_versions: Vec, } + +/// Details carried in the \`data\` field of a \`CanvasProviderError\` (-32012) +/// error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasProviderErrorData { + /// Provider-defined error code identifying the failure. + pub code: String, + /// Human-readable error message. + pub message: String, +} `; } @@ -1820,11 +1867,13 @@ function checkExhaustiveness(project: Project): void { 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'CanvasProviderSource', // CANVAS_PROVIDER_SOURCE_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', + 'CanvasProviderErrorData', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index d32c7630..4566404b 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -114,6 +114,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' + || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | CanvasState | AnnotationsState | ChatState' || tsType === 'RootState | SessionState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' @@ -546,6 +547,7 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'CanvasAvailability', 'CanvasProviderKind', ]; const STATE_STRUCTS = [ @@ -596,6 +598,8 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', + 'SessionCanvasAction', 'SessionCanvasDeclaration', 'ClientCanvasDeclaration', 'OpenCanvasRef', + 'CanvasServerProviderSource', 'CanvasClientProviderSource', 'CanvasState', ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -795,6 +799,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { ], }; +const CANVAS_PROVIDER_SOURCE_UNION: UnionConfig = { + name: 'CanvasProviderSource', + discriminantField: 'kind', + // Open union: future protocol versions may add new canvas provider kinds. + allowUnknown: true, + variants: [ + { caseName: 'server', structName: 'CanvasServerProviderSource', discriminantValue: 'server' }, + { caseName: 'client', structName: 'CanvasClientProviderSource', discriminantValue: 'client' }, + ], +}; + function generateToolResultContentUnion(): string { return `public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) @@ -885,7 +900,7 @@ public enum StringOrMarkdown: Codable, Sendable, Equatable { } function generateSnapshotState(): string { - return `/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, or annotations state. + return `/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, canvas, or annotations state. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) @@ -893,6 +908,7 @@ public enum SnapshotState: Codable, Sendable { case terminal(TerminalState) case changeset(ChangesetState) case resourceWatch(ResourceWatchState) + case canvas(CanvasState) case annotations(AnnotationsState) public init(from decoder: Decoder) throws { @@ -910,6 +926,8 @@ public enum SnapshotState: Codable, Sendable { self = .changeset(changeset) } else if let resourceWatch = try? ResourceWatchState(from: decoder) { self = .resourceWatch(resourceWatch) + } else if let canvas = try? CanvasState(from: decoder) { + self = .canvas(canvas) } else if let annotations = try? AnnotationsState(from: decoder) { self = .annotations(annotations) } else { @@ -925,6 +943,7 @@ public enum SnapshotState: Codable, Sendable { case .terminal(let state): try state.encode(to: encoder) case .changeset(let state): try state.encode(to: encoder) case .resourceWatch(let state): try state.encode(to: encoder) + case .canvas(let state): try state.encode(to: encoder) case .annotations(let state): try state.encode(to: encoder) } } @@ -1057,6 +1076,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(CANVAS_PROVIDER_SOURCE_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1099,6 +1120,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/activityChanged', caseName: 'sessionActivityChanged', tsInterface: 'SessionActivityChangedAction' }, { type: 'session/changesetsChanged', caseName: 'sessionChangesetsChanged', tsInterface: 'SessionChangesetsChangedAction' }, { type: 'session/serverToolsChanged', caseName: 'sessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, + { type: 'session/canvasesChanged', caseName: 'sessionCanvasesChanged', tsInterface: 'SessionCanvasesChangedAction' }, + { type: 'session/openCanvasesChanged', caseName: 'sessionOpenCanvasesChanged', tsInterface: 'SessionOpenCanvasesChangedAction' }, { type: 'session/activeClientSet', caseName: 'sessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', caseName: 'sessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, { type: 'session/inputNeededSet', caseName: 'sessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, @@ -1144,6 +1167,9 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'terminal/commandExecuted', caseName: 'terminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', caseName: 'terminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', caseName: 'resourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'canvas/updated', caseName: 'canvasUpdated', tsInterface: 'CanvasUpdatedAction' }, + { type: 'canvas/closeRequested', caseName: 'canvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, + { type: 'canvas/message', caseName: 'canvasMessage', tsInterface: 'CanvasMessageAction' }, ]; /** Merged struct for the approved/denied tool call confirmed action */ @@ -1340,6 +1366,10 @@ const COMMAND_STRUCTS = [ 'CompletionsParams', 'CompletionItem', 'CompletionsResult', 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', + 'CanvasOpenParams', 'CanvasOpenResult', + 'CanvasInvokeActionParams', 'CanvasInvokeActionResult', + 'CanvasCloseParams', + 'CanvasReadResourceParams', 'CanvasReadResourceResult', 'CanvasResourceContent', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1569,11 +1599,15 @@ function generateErrorsFile(project: Project): string { lines.push(' public static let permissionDenied = -32009'); lines.push(' /// The target resource already exists and the operation does not allow overwriting'); lines.push(' public static let alreadyExists = -32010'); + lines.push(' /// An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state'); + lines.push(' public static let conflict = -32011'); + lines.push(' /// A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }`'); + lines.push(' public static let canvasProviderError = -32012'); lines.push('}'); lines.push(''); lines.push('// MARK: - Error Detail Payloads\n'); - for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData']) { + for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'CanvasProviderErrorData']) { try { lines.push(generateStructFromInterface(project, ifaceName)); lines.push(''); @@ -1931,10 +1965,12 @@ function checkExhaustiveness(project: Project): void { 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'CanvasProviderSource', // CANVAS_PROVIDER_SOURCE_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() + 'CanvasProviderErrorData', // emitted by generateErrorsFile() 'AhpError', // typed via JsonRpcError; not a Swift struct 'AhpErrorDetailsMap', // type-level mapping; not a Swift struct 'AhpErrorCode', // type-level alias over AhpErrorCodes const enum diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 150dbc1a..832c56d5 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -78,6 +78,11 @@ import type { TerminalCommandExecutedAction, TerminalCommandFinishedAction, ResourceWatchChangedAction, + SessionCanvasesChangedAction, + SessionOpenCanvasesChangedAction, + CanvasUpdatedAction, + CanvasCloseRequestedAction, + CanvasMessageAction, } from './actions.js'; import { ActionType } from './actions.js'; @@ -129,6 +134,8 @@ export type SessionAction = | SessionChangesetsChangedAction | SessionConfigChangedAction | SessionMetaChangedAction + | SessionCanvasesChangedAction + | SessionOpenCanvasesChangedAction ; /** Union of session actions that clients may dispatch. */ @@ -160,6 +167,8 @@ export type ServerSessionAction = | SessionActivityChangedAction | SessionChangesetsChangedAction | SessionMetaChangedAction + | SessionCanvasesChangedAction + | SessionOpenCanvasesChangedAction ; /** Union of all chat-scoped actions. */ @@ -321,6 +330,24 @@ export type ServerResourceWatchAction = | ResourceWatchChangedAction ; +/** Union of all canvas-scoped actions. */ +export type CanvasAction = + | CanvasUpdatedAction + | CanvasCloseRequestedAction + | CanvasMessageAction +; + +/** Union of canvas actions that clients may dispatch. */ +export type ClientCanvasAction = + | CanvasCloseRequestedAction + | CanvasMessageAction +; + +/** Union of canvas actions that only the server may produce. */ +export type ServerCanvasAction = + | CanvasUpdatedAction +; + // ─── Client-Dispatchable Map ───────────────────────────────────────────────── /** @@ -403,4 +430,9 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.TerminalCommandExecuted]: false, [ActionType.TerminalCommandFinished]: false, [ActionType.ResourceWatchChanged]: false, + [ActionType.SessionCanvasesChanged]: false, + [ActionType.SessionOpenCanvasesChanged]: false, + [ActionType.CanvasUpdated]: false, + [ActionType.CanvasCloseRequested]: true, + [ActionType.CanvasMessage]: true, }; diff --git a/types/actions.ts b/types/actions.ts index a2469d66..f792f39d 100644 --- a/types/actions.ts +++ b/types/actions.ts @@ -15,3 +15,4 @@ export * from './channels-terminal/actions.js'; export * from './channels-changeset/actions.js'; export * from './channels-annotations/actions.js'; export * from './channels-resource-watch/actions.js'; +export * from './channels-canvas/actions.js'; diff --git a/types/channels-canvas/actions.ts b/types/channels-canvas/actions.ts new file mode 100644 index 00000000..25e8ef03 --- /dev/null +++ b/types/channels-canvas/actions.ts @@ -0,0 +1,73 @@ +/** + * Canvas Channel Actions — Mutations of an `ahp-canvas:` channel's state. + * + * @module channels-canvas/actions + */ + +import { ActionType } from '../common/actions.js'; +import type { CanvasAvailability } from '../channels-session/state.js'; + +// ─── Canvas Actions ────────────────────────────────────────────────────────── + +/** + * The canvas instance's presentation state changed. + * + * Sparse-merge semantics: each present field overwrites the corresponding + * {@link CanvasState} field, and an absent field preserves the current value. + * There is no clear-to-absent via this action — that three-state distinction + * cannot survive JSON transport uniformly across languages, so a provider that + * needs to reset a field re-publishes it, and a full reset arrives as a fresh + * {@link CanvasState} snapshot on (re)subscribe. + * + * @category Canvas Actions + * @version 1 + */ +export interface CanvasUpdatedAction { + type: ActionType.CanvasUpdated; + /** New title. Absent preserves the current title. */ + title?: string; + /** New provider-defined status. Absent preserves the current status. */ + status?: string; + /** New content address. Absent preserves the current url. */ + url?: string; + /** New availability. Absent preserves the current availability. */ + availability?: CanvasAvailability; +} + +/** + * The user asked to close this canvas (e.g. hit the ✕ on the surface). + * + * A pure client→host signal with a no-op reducer, mirroring how + * `terminal/input` is a side-effect-only client action. The host runs the + * close flow in response — resolving `canvasClose` against the provider and + * dropping the instance from {@link SessionState.openCanvases} — rather than + * the reducer mutating channel state. + * + * @category Canvas Actions + * @version 1 + * @clientDispatchable + */ +export interface CanvasCloseRequestedAction { + type: ActionType.CanvasCloseRequested; +} + +/** + * An opaque message relayed between the rendered canvas View and the + * instance's provider — the relay-carried analogue of a `postMessage` bridge. + * + * Bidirectional: a client dispatches it to carry a View→provider message, and + * the host emits it to carry a provider→View message (routed to the provider + * resolved for the instance, or handled host-internally for a server-side + * provider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it + * is a pure signal with a no-op reducer, so it never bloats channel state. See + * {@link /specification/canvas-channel | Canvas Channel}. + * + * @category Canvas Actions + * @version 1 + * @clientDispatchable + */ +export interface CanvasMessageAction { + type: ActionType.CanvasMessage; + /** Opaque, provider-defined message payload. */ + payload: unknown; +} diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts new file mode 100644 index 00000000..8046970f --- /dev/null +++ b/types/channels-canvas/commands.ts @@ -0,0 +1,221 @@ +/** + * Canvas Channel Commands — the server → client provider request family + * (`canvasOpen` / `canvasInvokeAction` / `canvasClose`) and the client → + * server `canvasReadResource` content-fetch request. + * + * @module channels-canvas/commands + */ + +import type { URI } from '../common/state.js'; +import type { BaseParams } from '../common/commands.js'; + +// ─── canvasOpen ──────────────────────────────────────────────────────────── + +/** + * Opens a canvas instance against its provider. + * + * Sent by the host to the client that declared the target canvas via + * {@link SessionActiveClient.canvasProviders} (a client that also declared + * {@link ClientCapabilities.canvas}). For a server-side provider the host + * resolves the open host-internally and emits no request. The provider returns + * the initial render target and presentation fields, which the host folds into + * the new instance's {@link CanvasState}. + * + * Mirrors the `resource*` precedent: registered in `ServerCommandMap` and + * mirrored in `CommandMap` for symmetry. A client normally never initiates it — + * the host is not a canvas provider — and a receiver SHOULD reject a request + * whose target is not one of its declared providers. + * + * @category Commands + * @method canvasOpen + * @direction Client ↔ Server + * @messageType Request + * @version 1 + * @throws `CanvasProviderError` (`-32012`) if the provider cannot open the + * canvas; `data` carries the provider-defined `{ code, message }`. + * @example + * ```jsonc + * // Server → Client + * { "jsonrpc": "2.0", "id": 41, "method": "canvasOpen", + * "params": { + * "channel": "ahp-session:/2f9c…", + * "canvasId": "diff", + * "extensionId": "acme.canvases", + * "instanceId": "inst-7", + * "input": { "path": "src/app.ts" } + * } } + * + * // Client → Server + * { "jsonrpc": "2.0", "id": 41, "result": { + * "url": "https://acme.example/canvas/inst-7", + * "title": "src/app.ts" + * } } + * ``` + */ +export interface CanvasOpenParams extends BaseParams { + /** The owning session channel URI (`ahp-session:/`). */ + channel: URI; + /** Provider-local canvas id to open. */ + canvasId: string; + /** Owning provider id. */ + extensionId: string; + /** Caller-minted handle for the new instance. */ + instanceId: string; + /** Open input, validated by the provider against its declared schema. */ + input?: Record; +} + +/** + * Result of the `canvasOpen` command. + */ +export interface CanvasOpenResult { + /** Initial content address for the instance (see {@link CanvasState.url}). */ + url?: string; + /** Initial title. */ + title?: string; + /** Initial provider-defined status. */ + status?: string; +} + +// ─── canvasInvokeAction ────────────────────────────────────────────────────── + +/** + * Invokes one of a canvas's declared actions against its provider. + * + * Sent by the host to the providing client (or resolved host-internally for a + * server-side provider) when the agent invokes a + * {@link SessionCanvasAction | declared action} on an open instance. The + * provider returns an opaque, provider-defined value. Registered symmetrically + * with the rest of the provider family (see {@link CanvasOpenParams}). + * + * @category Commands + * @method canvasInvokeAction + * @direction Client ↔ Server + * @messageType Request + * @version 1 + * @throws `CanvasProviderError` (`-32012`) if the provider has no handler for + * the action (`canvas_action_no_handler`) or the invocation fails; `data` + * carries the provider-defined `{ code, message }`. + */ +export interface CanvasInvokeActionParams extends BaseParams { + /** The owning session channel URI (`ahp-session:/`). */ + channel: URI; + /** Instance handle the action targets. */ + instanceId: string; + /** Provider-local canvas id of the instance. */ + canvasId: string; + /** Owning provider id. */ + extensionId: string; + /** Declared action name to invoke. */ + actionName: string; + /** Action input, validated by the provider against its declared schema. */ + input?: Record; +} + +/** + * Result of the `canvasInvokeAction` command. + */ +export interface CanvasInvokeActionResult { + /** Opaque, provider-defined return value. */ + value?: unknown; +} + +// ─── canvasClose ───────────────────────────────────────────────────────────── + +/** + * Closes a canvas instance against its provider. + * + * Sent by the host to the providing client (or resolved host-internally for a + * server-side provider) as part of the close flow — typically after a client + * dispatches `canvas/closeRequested`. The host then drops the instance from + * {@link SessionState.openCanvases}. Registered symmetrically with the rest of + * the provider family (see {@link CanvasOpenParams}). + * + * @category Commands + * @method canvasClose + * @direction Client ↔ Server + * @messageType Request + * @version 1 + */ +export interface CanvasCloseParams extends BaseParams { + /** The owning session channel URI (`ahp-session:/`). */ + channel: URI; + /** Instance handle to close. */ + instanceId: string; + /** Provider-local canvas id of the instance. */ + canvasId: string; + /** Owning provider id. */ + extensionId: string; +} + +// ─── canvasReadResource ────────────────────────────────────────────────────── + +/** + * Reads channel-served canvas content by `ahp-canvas-content:` URI. + * + * A client → host request, modeled on MCP's `resources/read`. When a canvas's + * {@link CanvasState.url} (or a sub-resource the rendered document references) + * is an `ahp-canvas-content://` address, the renderer cannot + * dial the host directly — for example in a relayed deployment behind a broker + * — so it resolves the bytes over the instance's existing `ahp-canvas:/` + * channel with this request instead of loading a network URL. The `` + * segment of the URI identifies which canvas channel to read from. See + * {@link /specification/canvas-channel | Canvas Channel}. + * + * @category Commands + * @method canvasReadResource + * @direction Client → Server + * @messageType Request + * @version 1 + * @throws `NotFound` (`-32008`) if the content URI does not resolve. + * @example + * ```jsonc + * // Client → Server + * { "jsonrpc": "2.0", "id": 52, "method": "canvasReadResource", + * "params": { + * "channel": "ahp-canvas:/9b1e…", + * "uri": "ahp-canvas-content:/inst-7/index.html" + * } } + * + * // Server → Client + * { "jsonrpc": "2.0", "id": 52, "result": { + * "contents": [ + * { "uri": "ahp-canvas-content:/inst-7/index.html", + * "mimeType": "text/html", "text": "…" } + * ] + * } } + * ``` + */ +export interface CanvasReadResourceParams extends BaseParams { + /** The owning canvas channel URI (`ahp-canvas:/`). */ + channel: URI; + /** An `ahp-canvas-content://` content URI to read. */ + uri: string; +} + +/** + * Result of the `canvasReadResource` command. + */ +export interface CanvasReadResourceResult { + /** The resolved content parts, wrapped for forward compatibility. */ + contents: CanvasResourceContent[]; +} + +/** + * One resolved piece of channel-served canvas content. + * + * Carries exactly one of {@link text} (text payloads) or {@link blob} + * (base64-encoded binary payloads). + * + * @category Commands + */ +export interface CanvasResourceContent { + /** The content URI this part resolves. */ + uri: string; + /** MIME type of the content, when known. */ + mimeType?: string; + /** UTF-8 text content, for text payloads. */ + text?: string; + /** Base64-encoded content, for binary payloads. */ + blob?: string; +} diff --git a/types/channels-canvas/reducer.ts b/types/channels-canvas/reducer.ts new file mode 100644 index 00000000..a29b6022 --- /dev/null +++ b/types/channels-canvas/reducer.ts @@ -0,0 +1,45 @@ +/** + * Canvas Channel Reducer — Pure reducer for `CanvasState`. + * + * @module channels-canvas/reducer + */ + +import { ActionType } from '../common/actions.js'; +import type { CanvasState } from './state.js'; +import type { CanvasAction } from '../action-origin.generated.js'; +import { softAssertNever } from '../common/reducer-helpers.js'; + +/** + * Pure reducer for canvas state. Handles all {@link CanvasAction} variants. + * + * `canvas/updated` is a sparse merge — a present field overwrites the + * corresponding {@link CanvasState} field and an absent field preserves the + * current value. `canvas/closeRequested` and `canvas/message` are pure + * signals with no state effect (the host acts on them out of band), mirroring + * how `terminal/input` is side-effect-only. + */ +export function canvasReducer(state: CanvasState, action: CanvasAction, log?: (msg: string) => void): CanvasState { + switch (action.type) { + case ActionType.CanvasUpdated: + return { + ...state, + title: action.title ?? state.title, + status: action.status ?? state.status, + url: action.url ?? state.url, + availability: action.availability ?? state.availability, + }; + + case ActionType.CanvasCloseRequested: + // Side-effect-only: a client→host "user hit ✕" signal. The host runs + // the close flow; the reducer keeps no state for it. + return state; + + case ActionType.CanvasMessage: + // Side-effect-only: an opaque View↔provider relay message. No state. + return state; + + default: + softAssertNever(action, log); + return state; + } +} diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts new file mode 100644 index 00000000..8a45c1df --- /dev/null +++ b/types/channels-canvas/state.ts @@ -0,0 +1,65 @@ +/** + * Canvas Channel State Types — Per-open-instance state exposed on + * `ahp-canvas:` channels. + * + * @module channels-canvas/state + */ + +import type { + CanvasAvailability, + CanvasProviderSource, +} from '../channels-session/state.js'; + +// ─── Canvas State ──────────────────────────────────────────────────────────── + +/** + * Full state for a single open canvas instance, delivered when a client + * subscribes to the instance's `ahp-canvas:/` channel. + * + * One channel exists per open instance — the same "one channel per resource" + * convention used by terminals, changesets, and resource watches. The + * lightweight catalogue entry that advertises this channel is + * {@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the + * authoritative, mutable per-instance view a renderer reads. + * + * Rendering is state-driven: a client renders the canvas by reading + * {@link url} and resolving it per the renderer's URL policy — directly for a + * reachable address, or over this channel via `canvasReadResource` for an + * `ahp-canvas-content:` address. It never receives a "render this" request. + * + * @category Canvas State + */ +export interface CanvasState { + /** Server-assigned instance handle, unique within the session. */ + instanceId: string; + /** Provider-local canvas id this instance was opened from. */ + canvasId: string; + /** Owning provider id. */ + extensionId: string; + /** Human-readable provider name. */ + extensionName?: string; + /** Human-readable canvas name. */ + displayName?: string; + /** + * Input the agent supplied when opening the instance. Retained so the + * instance can be resumed or rebound after a reconnect. + */ + input?: Record; + /** Current instance title. */ + title?: string; + /** Provider-defined status string (opaque to AHP). */ + status?: string; + /** + * Renderer-targeted address for the opaque canvas content — either a + * directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) + * or a channel-served `ahp-canvas-content://` address the + * renderer resolves over this channel with `canvasReadResource`. The + * renderer dispatches on the scheme and enforces its URL policy. See + * {@link /specification/canvas-channel | Canvas Channel}. + */ + url?: string; + /** Whether this instance's provider is currently available. */ + availability: CanvasAvailability; + /** Which provider owns the callbacks (`canvasOpen` / … ) for this instance. */ + provider: CanvasProviderSource; +} diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 6cf40e58..f0138ca1 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -10,6 +10,8 @@ import type { ToolDefinition, SessionActiveClient, SessionInputRequest, + SessionCanvasDeclaration, + OpenCanvasRef, Customization, McpServerState, } from './state.js'; @@ -255,6 +257,41 @@ export interface SessionActiveClientRemovedAction { clientId: string; } +// ─── Canvas Actions ────────────────────────────────────────────────────────── + +/** + * The aggregated canvas registry for this session changed. + * + * Full-replacement semantics: the `canvases` array replaces + * {@link SessionState.canvases} entirely, mirroring + * `session/serverToolsChanged`. The host republishes the union of every + * connected provider (server-side and client-declared) whenever it changes. + * + * @category Session Actions + * @version 1 + */ +export interface SessionCanvasesChangedAction { + type: ActionType.SessionCanvasesChanged; + /** Updated canvas registry (full replacement). */ + canvases: SessionCanvasDeclaration[]; +} + +/** + * The catalogue of open canvas instances for this session changed. + * + * Full-replacement semantics: the `openCanvases` array replaces + * {@link SessionState.openCanvases} entirely. The host republishes the + * catalogue as instances open and close. + * + * @category Session Actions + * @version 1 + */ +export interface SessionOpenCanvasesChangedAction { + type: ActionType.SessionOpenCanvasesChanged; + /** Updated open-instance catalogue (full replacement). */ + openCanvases: OpenCanvasRef[]; +} + // ─── Input Needed Actions ──────────────────────────────────────────────────── /** diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index ca0e2985..eb09859c 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -156,6 +156,12 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: case ActionType.SessionServerToolsChanged: return { ...state, serverTools: action.tools }; + case ActionType.SessionCanvasesChanged: + return { ...state, canvases: action.canvases }; + + case ActionType.SessionOpenCanvasesChanged: + return { ...state, openCanvases: action.openCanvases }; + case ActionType.SessionActiveClientSet: { const list = state.activeClients; const idx = list.findIndex(c => c.clientId === action.activeClient.clientId); diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 2e875e65..ce8b3690 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -186,6 +186,29 @@ export interface SessionState extends SessionMetadata { * once the underlying request resolves. */ inputNeeded?: SessionInputRequest[]; + /** + * Aggregated canvas registry currently exposed to the agent — the union of + * every connected provider (server-side and client-declared). Each entry + * describes a canvas the agent can open; the host folds + * {@link SessionActiveClient.canvasProviders | client-declared providers} + * into this list alongside its own server-side providers. + * + * Full-replacement via `session/canvasesChanged`. Populated only when at + * least one connected client declared {@link ClientCapabilities.canvas}; + * absent for sessions with no canvas surface. See + * {@link /specification/canvas-channel | Canvas Channel}. + */ + canvases?: SessionCanvasDeclaration[]; + /** + * Lightweight catalogue of currently-open canvas instances. Each entry + * carries the instance's `ahp-canvas:/` channel URI so a subscriber can + * subscribe to the full {@link CanvasState} and render it — analogous to how + * {@link RootState.terminals} catalogues live terminals whose full state + * lives on each terminal channel. + * + * Full-replacement via `session/openCanvasesChanged`. + */ + openCanvases?: OpenCanvasRef[]; /** * Additional provider-specific metadata for this session. * @@ -221,9 +244,182 @@ export interface SessionActiveClient { * children inside {@link SessionState.customizations}. */ customizations?: ClientPluginCustomization[]; + /** + * Canvas declarations this client contributes as a provider. Published + * atomically with the rest of the active-client entry via + * `session/activeClientSet` — exactly like {@link tools} — so there is no + * separate canvas-provider change action. The host folds these into + * {@link SessionState.canvases} with + * {@link SessionCanvasDeclaration.source | `source`} set to + * `{ kind: 'client', clientId }` and routes + * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back + * to this client. Only meaningful for a client that declared + * {@link ClientCapabilities.canvas}. + */ + canvasProviders?: ClientCanvasDeclaration[]; +} + +// ─── Canvas Declarations ───────────────────────────────────────────────────── + +/** + * Availability of a canvas provider or open instance. + * + * @category Canvas Types + */ +export const enum CanvasAvailability { + /** The provider is connected and can service requests for this canvas. */ + Ready = 'ready', + /** + * The provider is temporarily unavailable (for example, a client provider + * that disconnected). The entry is retained so it can be restored when the + * provider reconnects; in-flight requests fail until then. + */ + Stale = 'stale', } -// ─── Session Input Requests ────────────────────────────────────────────────── +/** + * Discriminant for {@link CanvasProviderSource}. + * + * @category Canvas Types + */ +export const enum CanvasProviderKind { + /** The canvas is provided by the host itself. */ + Server = 'server', + /** The canvas is provided by a connected client. */ + Client = 'client', +} + +/** + * A canvas provided by the host. Carries no `clientId` — server-side provider + * requests are resolved host-internally rather than routed to a peer. + * + * @category Canvas Types + */ +export interface CanvasServerProviderSource { + kind: CanvasProviderKind.Server; +} + +/** + * A canvas provided by a connected client. The host routes + * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas + * to the identified client. + * + * @category Canvas Types + */ +export interface CanvasClientProviderSource { + kind: CanvasProviderKind.Client; + /** `clientId` of the providing client (matches `initialize`). */ + clientId: string; +} + +/** + * Where a canvas declaration came from — used for request routing and for + * cleaning the entry up when its provider disconnects. Modeled as a + * discriminated union so the `clientId` is only present (and required) for the + * client variant. + * + * @category Canvas Types + */ +export type CanvasProviderSource = + | CanvasServerProviderSource + | CanvasClientProviderSource; + +/** + * One named action a canvas exposes to the agent, mirroring the shape of a + * {@link ToolDefinition} entry. Unique within its owning + * `(extensionId, canvasId)`. + * + * @category Canvas Types + */ +export interface SessionCanvasAction { + /** Action name, unique within the owning `(extensionId, canvasId)`. */ + name: string; + /** Human-readable description of what the action does. */ + description?: string; + /** + * JSON Schema for the action's input. Opaque to AHP; mirrors the + * {@link ToolDefinition.inputSchema} shape. + */ + inputSchema?: Record; +} + +/** + * One entry in the aggregated {@link SessionState.canvases} registry — a canvas + * the agent can open, contributed by a server-side or client-declared provider. + * + * @category Canvas Types + */ +export interface SessionCanvasDeclaration { + /** Owning provider id. Stable across declarations and instances. */ + extensionId: string; + /** Human-readable provider name. */ + extensionName?: string; + /** Provider-local canvas id. Unique within `extensionId`. */ + canvasId: string; + /** Human-readable canvas name. */ + displayName: string; + /** Human-readable description of the canvas. */ + description: string; + /** + * JSON Schema for the canvas's open input. Opaque to AHP; mirrors the + * {@link ToolDefinition.inputSchema} shape. + */ + inputSchema?: Record; + /** Actions this canvas exposes to the agent. */ + actions?: SessionCanvasAction[]; + /** Where the declaration came from — for routing and cleanup. */ + source: CanvasProviderSource; +} + +/** + * The lighter declaration shape a client publishes on + * {@link SessionActiveClient.canvasProviders}. The host derives the + * `extensionId` and {@link SessionCanvasDeclaration.source | `source`} when + * folding it into {@link SessionState.canvases}. + * + * @category Canvas Types + */ +export interface ClientCanvasDeclaration { + /** Provider-local canvas id, unique within the publishing client. */ + canvasId: string; + /** Human-readable canvas name. */ + displayName: string; + /** Human-readable description of the canvas. */ + description: string; + /** JSON Schema for the canvas's open input. Opaque to AHP. */ + inputSchema?: Record; + /** Actions this canvas exposes to the agent. */ + actions?: SessionCanvasAction[]; +} + +/** + * A lightweight catalogue entry for one open canvas instance, surfaced on + * {@link SessionState.openCanvases}. The authoritative, mutable per-instance + * state lives on the instance's own {@link CanvasState} channel; this entry + * exists so a subscriber can discover the channel URI and render it without + * subscribing to every instance. + * + * @category Canvas Types + */ +export interface OpenCanvasRef { + /** Server-assigned instance handle, unique within the session. */ + instanceId: string; + /** + * The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load + * the full {@link CanvasState}. + */ + channel: URI; + /** Provider-local canvas id this instance was opened from. */ + canvasId: string; + /** Owning provider id. */ + extensionId: string; + /** Human-readable provider name. */ + extensionName?: string; + /** Current instance title, mirrored from {@link CanvasState.title}. */ + title?: string; + /** Whether the instance's provider is currently available. */ + availability: CanvasAvailability; +} /** * Discriminant for the kinds of outstanding input a session can surface in diff --git a/types/commands.ts b/types/commands.ts index c77dd949..8602e1c3 100644 --- a/types/commands.ts +++ b/types/commands.ts @@ -14,3 +14,4 @@ export * from './channels-chat/commands.js'; export * from './channels-terminal/commands.js'; export * from './channels-changeset/commands.js'; export * from './channels-resource-watch/commands.js'; +export * from './channels-canvas/commands.js'; diff --git a/types/common/actions.ts b/types/common/actions.ts index 474dc9c3..8f5cc471 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -39,6 +39,8 @@ import type { SessionChangesetsChangedAction, SessionConfigChangedAction, SessionMetaChangedAction, + SessionCanvasesChangedAction, + SessionOpenCanvasesChangedAction, } from '../channels-session/actions.js'; import type { @@ -104,6 +106,12 @@ import type { ResourceWatchChangedAction, } from '../channels-resource-watch/actions.js'; +import type { + CanvasUpdatedAction, + CanvasCloseRequestedAction, + CanvasMessageAction, +} from '../channels-canvas/actions.js'; + // ─── Action Type Enum ──────────────────────────────────────────────────────── /** @@ -187,6 +195,11 @@ export const enum ActionType { TerminalCommandExecuted = 'terminal/commandExecuted', TerminalCommandFinished = 'terminal/commandFinished', ResourceWatchChanged = 'resourceWatch/changed', + SessionCanvasesChanged = 'session/canvasesChanged', + SessionOpenCanvasesChanged = 'session/openCanvasesChanged', + CanvasUpdated = 'canvas/updated', + CanvasCloseRequested = 'canvas/closeRequested', + CanvasMessage = 'canvas/message', } // ─── Action Envelope ───────────────────────────────────────────────────────── @@ -297,4 +310,9 @@ export type StateAction = | TerminalCommandDetectionAvailableAction | TerminalCommandExecutedAction | TerminalCommandFinishedAction - | ResourceWatchChangedAction; + | ResourceWatchChangedAction + | SessionCanvasesChangedAction + | SessionOpenCanvasesChangedAction + | CanvasUpdatedAction + | CanvasCloseRequestedAction + | CanvasMessageAction; diff --git a/types/common/commands.ts b/types/common/commands.ts index c674d52e..3d0a4dac 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -167,6 +167,18 @@ export interface ClientCapabilities { * App-bearing tool calls as ordinary MCP tool calls. */ mcpApps?: Record; + /** + * Client can render canvases and host client-declared canvas providers — it + * can render an opaque canvas URL in an isolated surface, and it can answer + * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + * it declares via {@link SessionActiveClient.canvasProviders}. + * + * Hosts SHOULD only populate {@link SessionState.canvases} / + * {@link SessionState.openCanvases} and only route canvas requests to a + * client that declared this capability. Clients that omit it see no canvas + * surface. See {@link /specification/canvas-channel | Canvas Channel}. + */ + canvas?: Record; } /** diff --git a/types/common/errors.ts b/types/common/errors.ts index 7c1d62bc..736f75a1 100644 --- a/types/common/errors.ts +++ b/types/common/errors.ts @@ -95,6 +95,15 @@ export const AhpErrorCodes = { * fresh token or surface the conflict to the user. */ Conflict: -32011, + /** + * A canvas provider request (`canvasOpen`, `canvasInvokeAction`, or + * `canvasClose`) failed. The `data` field of the JSON-RPC error MUST be a + * {@link CanvasProviderErrorData} carrying the provider-defined + * `{ code, message }` — for example a `canvas_action_no_handler` code when + * the provider declared the canvas but has no handler for the requested + * action, or `canvas_provider_unavailable` when the provider is disconnected. + */ + CanvasProviderError: -32012, } as const; /** Union type of all AHP application error codes. */ @@ -157,6 +166,24 @@ export interface UnsupportedProtocolVersionErrorData { supportedVersions: string[]; } +/** + * Details carried in the `data` field of a `CanvasProviderError` (-32012) + * error. + * + * The `code` is a provider-defined string (opaque to AHP) identifying the + * failure — observed values include `canvas_action_no_handler` and + * `canvas_provider_unavailable`. `message` is a human-readable description. + * + * @category Error Details + * @version 1 + */ +export interface CanvasProviderErrorData { + /** Provider-defined error code identifying the failure. */ + code: string; + /** Human-readable error message. */ + message: string; +} + /** * Maps each AHP error code that carries structured `data` to the type of * that data. @@ -171,6 +198,7 @@ export interface AhpErrorDetailsMap { [AhpErrorCodes.AuthRequired]: AuthRequiredErrorData; [AhpErrorCodes.PermissionDenied]: PermissionDeniedErrorData; [AhpErrorCodes.UnsupportedProtocolVersion]: UnsupportedProtocolVersionErrorData; + [AhpErrorCodes.CanvasProviderError]: CanvasProviderErrorData; } /** AHP error codes that carry a structured `data` payload. */ diff --git a/types/common/messages.ts b/types/common/messages.ts index 14918c98..2b9f2410 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -70,6 +70,15 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult, } from '../channels-changeset/commands.js'; +import type { + CanvasOpenParams, + CanvasOpenResult, + CanvasInvokeActionParams, + CanvasInvokeActionResult, + CanvasCloseParams, + CanvasReadResourceParams, + CanvasReadResourceResult, +} from '../channels-canvas/commands.js'; import type { ActionEnvelope } from './actions.js'; import type { @@ -174,6 +183,10 @@ export interface CommandMap { 'sessionConfigCompletions': { params: SessionConfigCompletionsParams; result: SessionConfigCompletionsResult }; 'completions': { params: CompletionsParams; result: CompletionsResult }; 'invokeChangesetOperation': { params: InvokeChangesetOperationParams; result: InvokeChangesetOperationResult }; + 'canvasOpen': { params: CanvasOpenParams; result: CanvasOpenResult }; + 'canvasInvokeAction': { params: CanvasInvokeActionParams; result: CanvasInvokeActionResult }; + 'canvasClose': { params: CanvasCloseParams; result: null }; + 'canvasReadResource': { params: CanvasReadResourceParams; result: CanvasReadResourceResult }; } /** @@ -188,6 +201,13 @@ export interface CommandMap { * `virtual://my-client/...` plugins) and to drive per-session filesystem * providers without the client having to re-implement the wire schema. * + * The `canvas*` provider family (`canvasOpen` / `canvasInvokeAction` / + * `canvasClose`) is server → client: the host drives a client-declared + * canvas provider. It is mirrored in {@link CommandMap} for symmetry with + * the `resource*` precedent; a client normally never initiates it, and a + * receiver SHOULD reject a request whose target is not one of its declared + * providers. + * * @category Commands */ export interface ServerCommandMap { @@ -201,6 +221,9 @@ export interface ServerCommandMap { 'resourceMkdir': { params: ResourceMkdirParams; result: ResourceMkdirResult }; 'resourceRequest': { params: ResourceRequestParams; result: ResourceRequestResult }; 'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult }; + 'canvasOpen': { params: CanvasOpenParams; result: CanvasOpenResult }; + 'canvasInvokeAction': { params: CanvasInvokeActionParams; result: CanvasInvokeActionResult }; + 'canvasClose': { params: CanvasCloseParams; result: null }; } // ─── Notification Maps ─────────────────────────────────────────────────────── diff --git a/types/common/state.ts b/types/common/state.ts index a451c5f1..a6041041 100644 --- a/types/common/state.ts +++ b/types/common/state.ts @@ -12,6 +12,7 @@ import type { SessionState } from '../channels-session/state.js'; import type { TerminalState } from '../channels-terminal/state.js'; import type { ChangesetState } from '../channels-changeset/state.js'; import type { ResourceWatchState } from '../channels-resource-watch/state.js'; +import type { CanvasState } from '../channels-canvas/state.js'; import type { AnnotationsState } from '../channels-annotations/state.js'; import type { ChatState } from '../channels-chat/state.js'; @@ -337,7 +338,7 @@ export interface Snapshot { /** The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) */ resource: URI; /** The current state of the resource */ - state: RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState; + state: RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | CanvasState | AnnotationsState | ChatState; /** The `serverSeq` at which this snapshot was taken. Subsequent actions will have `serverSeq > fromSeq`. */ fromSeq: number; } diff --git a/types/index.ts b/types/index.ts index ccec8598..a66c1268 100644 --- a/types/index.ts +++ b/types/index.ts @@ -110,6 +110,14 @@ export type { TelemetryCapabilities, ResourceWatchState, ResourceChange, + CanvasState, + SessionCanvasAction, + SessionCanvasDeclaration, + ClientCanvasDeclaration, + OpenCanvasRef, + CanvasServerProviderSource, + CanvasClientProviderSource, + CanvasProviderSource, } from './state.js'; export { @@ -137,6 +145,8 @@ export { ChangesetOperationStatus, ChangesetOperationScope, ResourceChangeType, + CanvasAvailability, + CanvasProviderKind, } from './state.js'; // Action types @@ -213,6 +223,11 @@ export type { TerminalCommandFinishedAction, TerminalCommandDetectionAvailableAction, ResourceWatchChangedAction, + SessionCanvasesChangedAction, + SessionOpenCanvasesChangedAction, + CanvasUpdatedAction, + CanvasCloseRequestedAction, + CanvasMessageAction, } from './actions.js'; export { ActionType } from './actions.js'; @@ -238,6 +253,9 @@ export type { ResourceWatchAction, ClientResourceWatchAction, ServerResourceWatchAction, + CanvasAction, + ClientCanvasAction, + ServerCanvasAction, } from './action-origin.generated.js'; export { IS_CLIENT_DISPATCHABLE } from './action-origin.generated.js'; @@ -251,6 +269,7 @@ export { changesetReducer, annotationsReducer, resourceWatchReducer, + canvasReducer, isClientDispatchable, } from './reducers.js'; @@ -322,6 +341,14 @@ export type { InvokeChangesetOperationResult, ChangesetOperationTarget, ChangesetOperationFollowUp, + CanvasOpenParams, + CanvasOpenResult, + CanvasInvokeActionParams, + CanvasInvokeActionResult, + CanvasCloseParams, + CanvasReadResourceParams, + CanvasReadResourceResult, + CanvasResourceContent, } from './commands.js'; export { ReconnectResultType, ContentEncoding, CompletionItemKind, ResourceType, ResourceWriteMode } from './commands.js'; @@ -377,6 +404,7 @@ export type { AhpErrorDetailsMap, AuthRequiredErrorData, PermissionDeniedErrorData, + CanvasProviderErrorData, } from './errors.js'; // Version registry diff --git a/types/messages.test.ts b/types/messages.test.ts index 591a9d7c..8fe726bb 100644 --- a/types/messages.test.ts +++ b/types/messages.test.ts @@ -33,6 +33,7 @@ function readChannelSources(baseName: string): string { 'channels-changeset', 'channels-annotations', 'channels-resource-watch', + 'channels-canvas', ]; return dirs .map(dir => { diff --git a/types/reducers.test.ts b/types/reducers.test.ts index f3a8c7de..e2bf929d 100644 --- a/types/reducers.test.ts +++ b/types/reducers.test.ts @@ -25,11 +25,12 @@ import { changesetReducer, annotationsReducer, resourceWatchReducer, + canvasReducer, isClientDispatchable, } from './reducers.js'; import { IS_CLIENT_DISPATCHABLE } from './action-origin.generated.js'; import { ActionType } from './actions.js'; -import type { RootState, SessionState, ChatState, TerminalState, ChangesetState, AnnotationsState, ResourceWatchState } from './state.js'; +import type { RootState, SessionState, ChatState, TerminalState, ChangesetState, AnnotationsState, ResourceWatchState, CanvasState } from './state.js'; import { SessionStatus, TurnState, @@ -54,6 +55,7 @@ function readChannelSources(baseName: string): string { 'channels-changeset', 'channels-annotations', 'channels-resource-watch', + 'channels-canvas', ]; return dirs .map(dir => { @@ -140,6 +142,8 @@ describe('reducer fixtures', () => { state = annotationsReducer(state as AnnotationsState, action as any); } else if (fixture.reducer === 'resourceWatch') { state = resourceWatchReducer(state as ResourceWatchState, action as any); + } else if (fixture.reducer === 'canvas') { + state = canvasReducer(state as CanvasState, action as any); } else { state = sessionReducer(state as SessionState, action as any); } diff --git a/types/reducers.ts b/types/reducers.ts index d916ba3a..759aa4af 100644 --- a/types/reducers.ts +++ b/types/reducers.ts @@ -12,4 +12,5 @@ export { terminalReducer } from './channels-terminal/reducer.js'; export { changesetReducer } from './channels-changeset/reducer.js'; export { annotationsReducer } from './channels-annotations/reducer.js'; export { resourceWatchReducer } from './channels-resource-watch/reducer.js'; +export { canvasReducer } from './channels-canvas/reducer.js'; export { softAssertNever, isClientDispatchable } from './common/reducer-helpers.js'; diff --git a/types/state.ts b/types/state.ts index 8748e48d..072cfaa5 100644 --- a/types/state.ts +++ b/types/state.ts @@ -16,3 +16,4 @@ export * from './channels-changeset/state.js'; export * from './channels-annotations/state.js'; export * from './channels-otlp/state.js'; export * from './channels-resource-watch/state.js'; +export * from './channels-canvas/state.js'; diff --git a/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json b/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json new file mode 100644 index 00000000..dca3d0b5 --- /dev/null +++ b/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json @@ -0,0 +1,65 @@ +{ + "description": "session/canvasesChanged replaces state.canvases with the new registry", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/canvasesChanged", + "canvases": [ + { + "extensionId": "acme.editors", + "extensionName": "Acme Editors", + "canvasId": "markdown", + "displayName": "Markdown Editor", + "description": "Edit and preview Markdown documents.", + "source": { "kind": "server" } + }, + { + "extensionId": "acme.viewers", + "canvasId": "image", + "displayName": "Image Viewer", + "description": "Preview raster and vector images.", + "actions": [ + { "name": "zoom", "description": "Zoom the image to a factor." } + ], + "source": { "kind": "client", "clientId": "client-42" } + } + ] + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "canvases": [ + { + "extensionId": "acme.editors", + "extensionName": "Acme Editors", + "canvasId": "markdown", + "displayName": "Markdown Editor", + "description": "Edit and preview Markdown documents.", + "source": { "kind": "server" } + }, + { + "extensionId": "acme.viewers", + "canvasId": "image", + "displayName": "Image Viewer", + "description": "Preview raster and vector images.", + "actions": [ + { "name": "zoom", "description": "Zoom the image to a factor." } + ], + "source": { "kind": "client", "clientId": "client-42" } + } + ], + "activeClients": [], + "chats": [] + } +} diff --git a/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json b/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json new file mode 100644 index 00000000..51d94580 --- /dev/null +++ b/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json @@ -0,0 +1,61 @@ +{ + "description": "session/openCanvasesChanged replaces state.openCanvases with the new catalogue", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/openCanvasesChanged", + "openCanvases": [ + { + "instanceId": "canvas-1", + "channel": "ahp-canvas:/canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "extensionName": "Acme Editors", + "title": "README.md", + "availability": "ready" + }, + { + "instanceId": "canvas-2", + "channel": "ahp-canvas:/canvas-2", + "canvasId": "image", + "extensionId": "acme.viewers", + "availability": "stale" + } + ] + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "openCanvases": [ + { + "instanceId": "canvas-1", + "channel": "ahp-canvas:/canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "extensionName": "Acme Editors", + "title": "README.md", + "availability": "ready" + }, + { + "instanceId": "canvas-2", + "channel": "ahp-canvas:/canvas-2", + "canvasId": "image", + "extensionId": "acme.viewers", + "availability": "stale" + } + ], + "activeClients": [], + "chats": [] + } +} diff --git a/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json b/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json new file mode 100644 index 00000000..db2ac17d --- /dev/null +++ b/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json @@ -0,0 +1,37 @@ +{ + "description": "canvas/updated overwrites every presented field (title, status, url, availability)", + "reducer": "canvas", + "initial": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "extensionName": "Acme Editors", + "displayName": "Markdown Editor", + "title": "Draft", + "status": "idle", + "url": "ahp-canvas-content:/canvas-1/index.html", + "availability": "ready", + "provider": { "kind": "server" } + }, + "actions": [ + { + "type": "canvas/updated", + "title": "Published", + "status": "saved", + "url": "https://example.com/docs/published.html", + "availability": "stale" + } + ], + "expected": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "extensionName": "Acme Editors", + "displayName": "Markdown Editor", + "title": "Published", + "status": "saved", + "url": "https://example.com/docs/published.html", + "availability": "stale", + "provider": { "kind": "server" } + } +} diff --git a/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json b/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json new file mode 100644 index 00000000..a26256f9 --- /dev/null +++ b/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json @@ -0,0 +1,31 @@ +{ + "description": "canvas/updated with only title and url present preserves the absent status and availability", + "reducer": "canvas", + "initial": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "status": "idle", + "url": "ahp-canvas-content:/canvas-1/index.html", + "availability": "ready", + "provider": { "kind": "client", "clientId": "client-42" } + }, + "actions": [ + { + "type": "canvas/updated", + "title": "Renamed", + "url": "https://example.com/docs/renamed.html" + } + ], + "expected": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Renamed", + "status": "idle", + "url": "https://example.com/docs/renamed.html", + "availability": "ready", + "provider": { "kind": "client", "clientId": "client-42" } + } +} diff --git a/types/test-cases/reducers/236-canvas-updated-partial-complementary.json b/types/test-cases/reducers/236-canvas-updated-partial-complementary.json new file mode 100644 index 00000000..17419321 --- /dev/null +++ b/types/test-cases/reducers/236-canvas-updated-partial-complementary.json @@ -0,0 +1,31 @@ +{ + "description": "canvas/updated with only status and availability present preserves the absent title and url", + "reducer": "canvas", + "initial": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "status": "idle", + "url": "ahp-canvas-content:/canvas-1/index.html", + "availability": "ready", + "provider": { "kind": "server" } + }, + "actions": [ + { + "type": "canvas/updated", + "status": "error", + "availability": "stale" + } + ], + "expected": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "status": "error", + "url": "ahp-canvas-content:/canvas-1/index.html", + "availability": "stale", + "provider": { "kind": "server" } + } +} diff --git a/types/test-cases/reducers/237-canvas-closerequested-no-op.json b/types/test-cases/reducers/237-canvas-closerequested-no-op.json new file mode 100644 index 00000000..ef06ab56 --- /dev/null +++ b/types/test-cases/reducers/237-canvas-closerequested-no-op.json @@ -0,0 +1,25 @@ +{ + "description": "canvas/closeRequested is a side-effect-only signal — the reducer keeps state unchanged", + "reducer": "canvas", + "initial": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "availability": "ready", + "provider": { "kind": "server" } + }, + "actions": [ + { + "type": "canvas/closeRequested" + } + ], + "expected": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "availability": "ready", + "provider": { "kind": "server" } + } +} diff --git a/types/test-cases/reducers/238-canvas-message-no-op.json b/types/test-cases/reducers/238-canvas-message-no-op.json new file mode 100644 index 00000000..b340763e --- /dev/null +++ b/types/test-cases/reducers/238-canvas-message-no-op.json @@ -0,0 +1,26 @@ +{ + "description": "canvas/message is a side-effect-only relay signal — the reducer keeps state unchanged", + "reducer": "canvas", + "initial": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "availability": "ready", + "provider": { "kind": "client", "clientId": "client-42" } + }, + "actions": [ + { + "type": "canvas/message", + "payload": { "kind": "scrollTo", "line": 120 } + } + ], + "expected": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "title": "Draft", + "availability": "ready", + "provider": { "kind": "client", "clientId": "client-42" } + } +} diff --git a/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json b/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json new file mode 100644 index 00000000..d21e3de6 --- /dev/null +++ b/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json @@ -0,0 +1,23 @@ +{ + "description": "canvas unknown action type is no-op", + "reducer": "canvas", + "initial": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "availability": "ready", + "provider": { "kind": "server" } + }, + "actions": [ + { + "type": "canvas/nonExistentAction" + } + ], + "expected": { + "instanceId": "canvas-1", + "canvasId": "markdown", + "extensionId": "acme.editors", + "availability": "ready", + "provider": { "kind": "server" } + } +} diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index 8da4dddf..e93cad05 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -76,7 +76,11 @@ type _ExpectedCommands = | 'resolveSessionConfig' | 'sessionConfigCompletions' | 'completions' - | 'invokeChangesetOperation'; + | 'invokeChangesetOperation' + | 'canvasOpen' + | 'canvasInvokeAction' + | 'canvasClose' + | 'canvasReadResource'; /** All methods annotated `@messageType Notification` (client → server). */ type _ExpectedClientNotifications = @@ -106,7 +110,10 @@ type _ExpectedServerCommands = | 'resourceResolve' | 'resourceMkdir' | 'resourceRequest' - | 'createResourceWatch'; + | 'createResourceWatch' + | 'canvasOpen' + | 'canvasInvokeAction' + | 'canvasClose'; // ─── Assertions ────────────────────────────────────────────────────────────── diff --git a/types/version/registry.ts b/types/version/registry.ts index 3969004c..68668a24 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -15,7 +15,7 @@ import type { ServerNotificationMap } from '../messages.js'; * * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string. */ -export const PROTOCOL_VERSION = '0.5.1'; +export const PROTOCOL_VERSION = '0.6.0'; /** * Every protocol version a client built from this source tree is willing @@ -34,6 +34,7 @@ export const PROTOCOL_VERSION = '0.5.1'; * `scripts/verify-release-metadata.ts`. */ export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ + '0.6.0', '0.5.1', '0.5.0', ]); @@ -151,6 +152,11 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.TerminalCommandExecuted]: '0.1.0', [ActionType.TerminalCommandFinished]: '0.1.0', [ActionType.ResourceWatchChanged]: '0.2.0', + [ActionType.SessionCanvasesChanged]: '0.6.0', + [ActionType.SessionOpenCanvasesChanged]: '0.6.0', + [ActionType.CanvasUpdated]: '0.6.0', + [ActionType.CanvasCloseRequested]: '0.6.0', + [ActionType.CanvasMessage]: '0.6.0', }; /** From c5ca36b0a8a2f1d5e4304d43dc474956808dda3d Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Wed, 1 Jul 2026 21:54:37 -0500 Subject: [PATCH 2/5] Fix Rust fmt and clippy in canvas reducer - Reflow the `ahp_types::state` import block to satisfy `cargo fmt`. - Copy `CanvasAvailability` by deref instead of `.clone()` (it is `Copy`), fixing `clippy::clone_on_copy` under `-D warnings`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- clients/rust/crates/ahp/src/reducers.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 688d154e..7869280a 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -58,15 +58,15 @@ use ahp_types::actions::{ ChatToolCallResultConfirmedAction, ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, AnnotationsState, CanvasState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, - ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, - PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, - SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, - TerminalContentPart, TerminalState, TerminalUnclassifiedPart, ToolCallCancellationReason, - ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason, - ToolCallContributor, ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, - ToolCallResponsePart, ToolCallRunningState, ToolCallState, ToolCallStreamingState, Turn, - TurnState, + ActiveTurn, AnnotationsState, CanvasState, ChangesetOperationStatus, ChangesetState, + ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, + Customization, ErrorInfo, PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, + RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, + TerminalCommandPart, TerminalContentPart, TerminalState, TerminalUnclassifiedPart, + ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, + ToolCallConfirmationReason, ToolCallContributor, ToolCallPendingConfirmationState, + ToolCallPendingResultConfirmationState, ToolCallResponsePart, ToolCallRunningState, + ToolCallState, ToolCallStreamingState, Turn, TurnState, }; /// What happened when an action was applied. @@ -1636,7 +1636,7 @@ pub fn apply_action_to_canvas(state: &mut CanvasState, action: &StateAction) -> state.url = Some(url.clone()); } if let Some(availability) = &a.availability { - state.availability = availability.clone(); + state.availability = *availability; } ReduceOutcome::Applied } From c5ff126eab491d3c93f52683516dd3e1744370fe Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Wed, 1 Jul 2026 22:29:09 -0500 Subject: [PATCH 3/5] Address canvas PR review: client-dispatchable canvas/updated + doc fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Peer review of the canvas PR surfaced one design gap and three doc issues, each validated against the code before changing: - canvas/updated is now @clientDispatchable. The PR already models client-side canvas providers (ClientCanvasDeclaration, the client CanvasProviderSource variant, CanvasAvailability.Stale), and title / availability are mirrored onto OpenCanvasRef for other subscribers, yet canvas/updated was server-only — leaving a client-side provider no wire path to push its own presentation changes. This mirrors the established terminal/titleChanged pattern (a client may originate it; the host validates, applies, and re-broadcasts). Regenerated: IS_CLIENT_DISPATCHABLE now maps canvas/updated to true, ClientCanvasAction gains it, and ServerCanvasAction becomes `never`. - Fixed the canvas-channel.md sequence diagram to reference the real session/activeClientSet action (there is no session/activeClientsChanged). - Documented that a View to provider canvas/message routes to the single resolved provider, and that multi-renderer provider to View targeting is deferred to the open render-targeting question. - Clarified the deliberate channel split in the command family (session-scoped provider RPCs vs. instance-scoped canvasReadResource). No client reducer/mirror changes are needed: the dispatchability split lives only in the canonical action-origin table; the per-language reducers switch on action type. Verified across TS core (291 tests), Rust (fmt + clippy + test), Go, Swift, Kotlin, the TS client, and the docs build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- clients/go/ahptypes/actions.generated.go | 9 +++++++++ clients/rust/crates/ahp-types/src/actions.rs | 9 +++++++++ docs/specification/canvas-channel.md | 14 +++++++++----- schema/actions.schema.json | 2 +- schema/commands.schema.json | 2 +- types/action-origin.generated.ts | 5 +++-- types/channels-canvas/actions.ts | 10 ++++++++++ 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 1b56fbb6..76ee77b5 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -1263,6 +1263,15 @@ type ResourceWatchChangedAction struct { // The canvas instance's presentation state changed. // +// Emitted by the server for a server-side provider, or dispatched by the client +// that provides the instance to push its own presentation changes — mirroring +// how `terminal/titleChanged` lets a client-owned terminal rename itself. This +// is the only path a client-side provider has to update the structured +// {@link CanvasState} (and the {@link OpenCanvasRef} fields mirrored from it) +// that other subscribers render. The host stays the authoritative reducer: it +// SHOULD reject an update from a client that is not the instance's resolved +// provider, then applies the merge and re-broadcasts to subscribers. +// // Sparse-merge semantics: each present field overwrites the corresponding // {@link CanvasState} field, and an absent field preserves the current value. // There is no clear-to-absent via this action — that three-state distinction diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 539427a1..a42119e1 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1499,6 +1499,15 @@ pub struct ResourceWatchChangedAction { /// The canvas instance's presentation state changed. /// +/// Emitted by the server for a server-side provider, or dispatched by the client +/// that provides the instance to push its own presentation changes — mirroring +/// how `terminal/titleChanged` lets a client-owned terminal rename itself. This +/// is the only path a client-side provider has to update the structured +/// {@link CanvasState} (and the {@link OpenCanvasRef} fields mirrored from it) +/// that other subscribers render. The host stays the authoritative reducer: it +/// SHOULD reject an update from a client that is not the instance's resolved +/// provider, then applies the merge and re-broadcasts to subscribers. +/// /// Sparse-merge semantics: each present field overwrites the corresponding /// {@link CanvasState} field, and an absent field preserves the current value. /// There is no clear-to-absent via this action — that three-state distinction diff --git a/docs/specification/canvas-channel.md b/docs/specification/canvas-channel.md index a1ab74b6..a79615ca 100644 --- a/docs/specification/canvas-channel.md +++ b/docs/specification/canvas-channel.md @@ -140,13 +140,15 @@ Three actions travel on the per-instance channel. Because the channel is scoped | Action | Client-dispatchable | Reducer effect | |---|:---:|---| -| `canvas/updated` | No | Sparse-merges `title` / `status` / `url` / `availability` into `CanvasState`. | +| `canvas/updated` | Yes | Sparse-merges `title` / `status` / `url` / `availability` into `CanvasState`. | | `canvas/closeRequested` | Yes | No-op — signals the host to run the close flow. | | `canvas/message` | Yes | No-op — opaque View ↔ provider message bridge. | -`canvas/updated` uses **sparse-merge** semantics: each present field overwrites the corresponding `CanvasState` field, and an absent field preserves the current value. There is no clear-to-absent through this action — a provider that needs to reset a field re-publishes it, and a full reset arrives as a fresh `CanvasState` snapshot on (re)subscribe. +`canvas/updated` is dispatched by the server for a server-side provider, or by the client that provides an instance to push its own presentation changes — the same client-dispatchable pattern as `terminal/titleChanged`, and the only way a client-side provider updates the structured `CanvasState` (and the `OpenCanvasRef` fields mirrored from it) that other subscribers render. The host stays the authoritative reducer: it SHOULD reject an update from a client that is not the instance's resolved provider, then applies the merge and re-broadcasts. -`canvas/closeRequested` and `canvas/message` are pure signals with no-op reducers, mirroring how `terminal/input` is a side-effect-only client action — they never bloat channel state. `canvas/closeRequested` is a client → host signal (the user hit ✕); the host responds by resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. `canvas/message` is bidirectional: a client dispatches it to carry a View → provider message, and the host emits it to carry a provider → View message. +It uses **sparse-merge** semantics: each present field overwrites the corresponding `CanvasState` field, and an absent field preserves the current value. There is no clear-to-absent through this action — a provider that needs to reset a field re-publishes it, and a full reset arrives as a fresh `CanvasState` snapshot on (re)subscribe. + +`canvas/closeRequested` and `canvas/message` are pure signals with no-op reducers, mirroring how `terminal/input` is a side-effect-only client action — they never bloat channel state. `canvas/closeRequested` is a client → host signal (the user hit ✕); the host responds by resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. `canvas/message` is bidirectional: a client dispatches it to carry a View → provider message, and the host emits it to carry a provider → View message. A View → provider message is routed to the instance's single resolved provider — relayed to that client when the provider and renderer are different clients, or handled host-internally for a server-side provider. Targeting a particular renderer for a provider → View message when an instance has more than one renderer is not yet specified; it depends on the still-open render-targeting question and is out of scope here. ## Provider request family @@ -160,7 +162,7 @@ sequenceDiagram participant C as Client (provider + renderer) Note over C: declares ClientCapabilities.canvas
publishes canvasProviders - C->>H: dispatchAction session/activeClientsChanged (canvasProviders) + C->>H: dispatchAction session/activeClientSet (canvasProviders) H->>C: action session/canvasesChanged (registry) A->>H: open canvas "diff" @@ -192,11 +194,13 @@ sequenceDiagram `canvasReadResource` is modeled on MCP's `resources/read`; each `CanvasResourceContent` carries the resolved `uri`, an optional `mimeType`, and exactly one of `text` (UTF-8) or `blob` (base64). +The channel split within the family is deliberate: the three provider operations are session-scoped RPCs and travel on the session channel (`ahp-session:/`), while `canvasReadResource` resolves content bytes and so targets the instance's own channel (`ahp-canvas:/`). + ### Actions | Action | Direction | |---|---| -| `canvas/updated` | host → client (over the standard `action` envelope) | +| `canvas/updated` | client → host (a client-side provider) **or** host → client | | `canvas/closeRequested` | client → host | | `canvas/message` | client → host **or** host → client | diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 15b0540a..2dc71180 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -1884,7 +1884,7 @@ }, "CanvasUpdatedAction": { "type": "object", - "description": "The canvas instance's presentation state changed.\n\nSparse-merge semantics: each present field overwrites the corresponding\n{@link CanvasState} field, and an absent field preserves the current value.\nThere is no clear-to-absent via this action — that three-state distinction\ncannot survive JSON transport uniformly across languages, so a provider that\nneeds to reset a field re-publishes it, and a full reset arrives as a fresh\n{@link CanvasState} snapshot on (re)subscribe.", + "description": "The canvas instance's presentation state changed.\n\nEmitted by the server for a server-side provider, or dispatched by the client\nthat provides the instance to push its own presentation changes — mirroring\nhow `terminal/titleChanged` lets a client-owned terminal rename itself. This\nis the only path a client-side provider has to update the structured\n{@link CanvasState} (and the {@link OpenCanvasRef} fields mirrored from it)\nthat other subscribers render. The host stays the authoritative reducer: it\nSHOULD reject an update from a client that is not the instance's resolved\nprovider, then applies the merge and re-broadcasts to subscribers.\n\nSparse-merge semantics: each present field overwrites the corresponding\n{@link CanvasState} field, and an absent field preserves the current value.\nThere is no clear-to-absent via this action — that three-state distinction\ncannot survive JSON transport uniformly across languages, so a provider that\nneeds to reset a field re-publishes it, and a full reset arrives as a fresh\n{@link CanvasState} snapshot on (re)subscribe.", "properties": { "type": { "$ref": "#/$defs/ActionType.CanvasUpdated" diff --git a/schema/commands.schema.json b/schema/commands.schema.json index f7f5e57c..ed30d4d7 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -7921,7 +7921,7 @@ }, "CanvasUpdatedAction": { "type": "object", - "description": "The canvas instance's presentation state changed.\n\nSparse-merge semantics: each present field overwrites the corresponding\n{@link CanvasState} field, and an absent field preserves the current value.\nThere is no clear-to-absent via this action — that three-state distinction\ncannot survive JSON transport uniformly across languages, so a provider that\nneeds to reset a field re-publishes it, and a full reset arrives as a fresh\n{@link CanvasState} snapshot on (re)subscribe.", + "description": "The canvas instance's presentation state changed.\n\nEmitted by the server for a server-side provider, or dispatched by the client\nthat provides the instance to push its own presentation changes — mirroring\nhow `terminal/titleChanged` lets a client-owned terminal rename itself. This\nis the only path a client-side provider has to update the structured\n{@link CanvasState} (and the {@link OpenCanvasRef} fields mirrored from it)\nthat other subscribers render. The host stays the authoritative reducer: it\nSHOULD reject an update from a client that is not the instance's resolved\nprovider, then applies the merge and re-broadcasts to subscribers.\n\nSparse-merge semantics: each present field overwrites the corresponding\n{@link CanvasState} field, and an absent field preserves the current value.\nThere is no clear-to-absent via this action — that three-state distinction\ncannot survive JSON transport uniformly across languages, so a provider that\nneeds to reset a field re-publishes it, and a full reset arrives as a fresh\n{@link CanvasState} snapshot on (re)subscribe.", "properties": { "type": { "$ref": "#/$defs/ActionType.CanvasUpdated" diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 832c56d5..edc9534d 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -339,13 +339,14 @@ export type CanvasAction = /** Union of canvas actions that clients may dispatch. */ export type ClientCanvasAction = + | CanvasUpdatedAction | CanvasCloseRequestedAction | CanvasMessageAction ; /** Union of canvas actions that only the server may produce. */ export type ServerCanvasAction = - | CanvasUpdatedAction + never ; // ─── Client-Dispatchable Map ───────────────────────────────────────────────── @@ -432,7 +433,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.ResourceWatchChanged]: false, [ActionType.SessionCanvasesChanged]: false, [ActionType.SessionOpenCanvasesChanged]: false, - [ActionType.CanvasUpdated]: false, + [ActionType.CanvasUpdated]: true, [ActionType.CanvasCloseRequested]: true, [ActionType.CanvasMessage]: true, }; diff --git a/types/channels-canvas/actions.ts b/types/channels-canvas/actions.ts index 25e8ef03..e5bcbe38 100644 --- a/types/channels-canvas/actions.ts +++ b/types/channels-canvas/actions.ts @@ -12,6 +12,15 @@ import type { CanvasAvailability } from '../channels-session/state.js'; /** * The canvas instance's presentation state changed. * + * Emitted by the server for a server-side provider, or dispatched by the client + * that provides the instance to push its own presentation changes — mirroring + * how `terminal/titleChanged` lets a client-owned terminal rename itself. This + * is the only path a client-side provider has to update the structured + * {@link CanvasState} (and the {@link OpenCanvasRef} fields mirrored from it) + * that other subscribers render. The host stays the authoritative reducer: it + * SHOULD reject an update from a client that is not the instance's resolved + * provider, then applies the merge and re-broadcasts to subscribers. + * * Sparse-merge semantics: each present field overwrites the corresponding * {@link CanvasState} field, and an absent field preserves the current value. * There is no clear-to-absent via this action — that three-state distinction @@ -21,6 +30,7 @@ import type { CanvasAvailability } from '../channels-session/state.js'; * * @category Canvas Actions * @version 1 + * @clientDispatchable */ export interface CanvasUpdatedAction { type: ActionType.CanvasUpdated; From 7f11f60701bca91d8170135c7b1fc1bc43d30af9 Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Wed, 1 Jul 2026 22:48:51 -0500 Subject: [PATCH 4/5] Address review: register canvas at current spec version; widen isClientDispatchable Copilot review follow-ups on the canvas PR: - Don't bump PROTOCOL_VERSION or invent a `## [0.6.0]` CHANGELOG heading in a feature PR. Per repo convention (AGENTS.md + the ea386ee precedent), new actions register at the *current* spec version and their bullets accumulate under `## [Unreleased]`; the version bump and `## [X.Y.Z]` rotation are a dedicated release chore. Revert PROTOCOL_VERSION 0.6.0 -> 0.5.1, drop 0.6.0 from SUPPORTED_PROTOCOL_VERSIONS, register the five canvas actions/session actions at 0.5.1, and fold the canvas bullet back under `## [Unreleased]`. Regenerate the per-client Version.generated.* and release-metadata.json. - Extend `isClientDispatchable` to accept `CanvasAction` and narrow to `ClientCanvasAction`, so consumers can validate the canvas channel's client-dispatchable actions (canvas/updated, canvas/closeRequested, canvas/message) without a cast. Add a canvas assertion to its test. The two mermaid comments (session/activeClientSet naming, canvas/updated direction) were already resolved in the prior review-response commit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 ---- clients/go/ahptypes/version.generated.go | 3 +-- clients/go/release-metadata.json | 1 - clients/kotlin/release-metadata.json | 1 - .../generated/Version.generated.kt | 3 +-- clients/rust/crates/ahp-types/src/version.rs | 4 ++-- clients/rust/release-metadata.json | 1 - .../Generated/Version.generated.swift | 3 +-- clients/swift/release-metadata.json | 1 - clients/typescript/release-metadata.json | 1 - types/common/reducer-helpers.ts | 4 +++- types/reducers.test.ts | 5 +++++ types/version/registry.ts | 13 ++++++------- 13 files changed, 19 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec33872..68e94429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,10 +23,6 @@ changes accumulate. Track in-flight protocol changes via PRs touching `NOTIFICATION_INTRODUCED_IN` maps in [`types/version/registry.ts`](types/version/registry.ts). -## [0.6.0] — Unreleased - -Spec version: `0.6.0` - ### Added - Optional `intention` field on `chat/toolCallStart` and every `ToolCallState` diff --git a/clients/go/ahptypes/version.generated.go b/clients/go/ahptypes/version.generated.go index 320619c1..8d270be4 100644 --- a/clients/go/ahptypes/version.generated.go +++ b/clients/go/ahptypes/version.generated.go @@ -6,13 +6,12 @@ package ahptypes // ProtocolVersion is the current protocol version (SemVer // MAJOR.MINOR.PATCH) that this generated source speaks. -const ProtocolVersion = "0.6.0" +const ProtocolVersion = "0.5.1" // supportedProtocolVersions backs [SupportedProtocolVersions] — held // in an unexported slice so callers cannot accidentally mutate the // shared backing array. var supportedProtocolVersions = []string{ - "0.6.0", "0.5.1", "0.5.0", } diff --git a/clients/go/release-metadata.json b/clients/go/release-metadata.json index 37023344..c8eed75a 100644 --- a/clients/go/release-metadata.json +++ b/clients/go/release-metadata.json @@ -2,7 +2,6 @@ "client": "go", "packageVersion": "0.5.0", "supportedProtocolVersions": [ - "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/kotlin/release-metadata.json b/clients/kotlin/release-metadata.json index f56205b0..d9e0f413 100644 --- a/clients/kotlin/release-metadata.json +++ b/clients/kotlin/release-metadata.json @@ -2,7 +2,6 @@ "client": "kotlin", "packageVersion": "0.5.0", "supportedProtocolVersions": [ - "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt index 666c8aab..c1a4c3cf 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt @@ -5,7 +5,7 @@ package com.microsoft.agenthostprotocol.generated /** * Current protocol version (SemVer `MAJOR.MINOR.PATCH`). */ -public const val PROTOCOL_VERSION: String = "0.6.0" +public const val PROTOCOL_VERSION: String = "0.5.1" /** * Every protocol version this library is willing to negotiate, ordered @@ -16,7 +16,6 @@ public const val PROTOCOL_VERSION: String = "0.6.0" * protocol versions if the host doesn't accept the newest one. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( - "0.6.0", "0.5.1", "0.5.0", ) diff --git a/clients/rust/crates/ahp-types/src/version.rs b/clients/rust/crates/ahp-types/src/version.rs index 8db522a4..5b04e5bc 100644 --- a/clients/rust/crates/ahp-types/src/version.rs +++ b/clients/rust/crates/ahp-types/src/version.rs @@ -5,7 +5,7 @@ #![allow(missing_docs)] /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -pub const PROTOCOL_VERSION: &str = "0.6.0"; +pub const PROTOCOL_VERSION: &str = "0.5.1"; /// Every protocol version this crate is willing to negotiate, ordered /// most-preferred-first. The first entry equals [`PROTOCOL_VERSION`]. @@ -13,4 +13,4 @@ pub const PROTOCOL_VERSION: &str = "0.6.0"; /// Consumers building `InitializeParams` should pass this slice (or a /// derived `Vec`) so the same client binary can fall back to /// older protocol versions if the host doesn't accept the newest one. -pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.6.0", "0.5.1", "0.5.0"]; +pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.5.1", "0.5.0"]; diff --git a/clients/rust/release-metadata.json b/clients/rust/release-metadata.json index dca82575..1cdb02a6 100644 --- a/clients/rust/release-metadata.json +++ b/clients/rust/release-metadata.json @@ -2,7 +2,6 @@ "client": "rust", "packageVersion": "0.5.0", "supportedProtocolVersions": [ - "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift index b170a858..d4690313 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift @@ -3,7 +3,7 @@ import Foundation /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -public let PROTOCOL_VERSION: String = "0.6.0" +public let PROTOCOL_VERSION: String = "0.5.1" /// Every protocol version this package is willing to negotiate, /// ordered most-preferred-first. The first entry equals @@ -13,7 +13,6 @@ public let PROTOCOL_VERSION: String = "0.6.0" /// `InitializeParams` so the same client binary can fall back to older /// protocol versions if the host doesn't accept the newest one. public let SUPPORTED_PROTOCOL_VERSIONS: [String] = [ - "0.6.0", "0.5.1", "0.5.0", ] diff --git a/clients/swift/release-metadata.json b/clients/swift/release-metadata.json index 328bb9fc..3765f19c 100644 --- a/clients/swift/release-metadata.json +++ b/clients/swift/release-metadata.json @@ -2,7 +2,6 @@ "client": "swift", "packageVersion": "0.5.0", "supportedProtocolVersions": [ - "0.6.0", "0.5.1", "0.5.0" ] diff --git a/clients/typescript/release-metadata.json b/clients/typescript/release-metadata.json index 8090197b..fd3c324d 100644 --- a/clients/typescript/release-metadata.json +++ b/clients/typescript/release-metadata.json @@ -2,7 +2,6 @@ "client": "typescript", "packageVersion": "0.5.0", "supportedProtocolVersions": [ - "0.6.0", "0.5.1", "0.5.0" ] diff --git a/types/common/reducer-helpers.ts b/types/common/reducer-helpers.ts index 7742f2af..acef55e0 100644 --- a/types/common/reducer-helpers.ts +++ b/types/common/reducer-helpers.ts @@ -16,6 +16,8 @@ import type { ClientChangesetAction, AnnotationsAction, ClientAnnotationsAction, + CanvasAction, + ClientCanvasAction, } from '../action-origin.generated.js'; import { IS_CLIENT_DISPATCHABLE } from '../action-origin.generated.js'; @@ -40,6 +42,6 @@ export function softAssertNever(value: never, log?: (msg: string) => void): void * Servers SHOULD call this to validate incoming `dispatchAction` requests * and reject any action the client is not allowed to originate. */ -export function isClientDispatchable(action: RootAction | SessionAction | TerminalAction | ChangesetAction | AnnotationsAction): action is ClientRootAction | ClientSessionAction | ClientTerminalAction | ClientChangesetAction | ClientAnnotationsAction { +export function isClientDispatchable(action: RootAction | SessionAction | TerminalAction | ChangesetAction | AnnotationsAction | CanvasAction): action is ClientRootAction | ClientSessionAction | ClientTerminalAction | ClientChangesetAction | ClientAnnotationsAction | ClientCanvasAction { return IS_CLIENT_DISPATCHABLE[action.type]; } diff --git a/types/reducers.test.ts b/types/reducers.test.ts index e2bf929d..4f03ac4c 100644 --- a/types/reducers.test.ts +++ b/types/reducers.test.ts @@ -223,6 +223,11 @@ describe('isClientDispatchable', () => { const action = { type: ActionType.SessionReady, session: 'x' } as const; assert.equal(isClientDispatchable(action), false); }); + + it('accepts and narrows client-dispatchable canvas actions', () => { + const action = { type: ActionType.CanvasCloseRequested } as const; + assert.equal(isClientDispatchable(action), true); + }); }); // ─── Immutability Checks ───────────────────────────────────────────────────── diff --git a/types/version/registry.ts b/types/version/registry.ts index 68668a24..e621c706 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -15,7 +15,7 @@ import type { ServerNotificationMap } from '../messages.js'; * * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string. */ -export const PROTOCOL_VERSION = '0.6.0'; +export const PROTOCOL_VERSION = '0.5.1'; /** * Every protocol version a client built from this source tree is willing @@ -34,7 +34,6 @@ export const PROTOCOL_VERSION = '0.6.0'; * `scripts/verify-release-metadata.ts`. */ export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ - '0.6.0', '0.5.1', '0.5.0', ]); @@ -152,11 +151,11 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.TerminalCommandExecuted]: '0.1.0', [ActionType.TerminalCommandFinished]: '0.1.0', [ActionType.ResourceWatchChanged]: '0.2.0', - [ActionType.SessionCanvasesChanged]: '0.6.0', - [ActionType.SessionOpenCanvasesChanged]: '0.6.0', - [ActionType.CanvasUpdated]: '0.6.0', - [ActionType.CanvasCloseRequested]: '0.6.0', - [ActionType.CanvasMessage]: '0.6.0', + [ActionType.SessionCanvasesChanged]: '0.5.1', + [ActionType.SessionOpenCanvasesChanged]: '0.5.1', + [ActionType.CanvasUpdated]: '0.5.1', + [ActionType.CanvasCloseRequested]: '0.5.1', + [ActionType.CanvasMessage]: '0.5.1', }; /** From 0960986ef3f6f396e97e283073bc328dd3be2cb8 Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Mon, 13 Jul 2026 03:22:33 -0500 Subject: [PATCH 5/5] Address canvas review: providerId rename, operation vocabulary, drop message/read paths Incorporate maintainer feedback on the canvas channel: - Rename the opaque `extensionId` namespace to `providerId` and drop `extensionName`; AHP treats it as an uninterpreted routing key that keeps provider-local `canvasId`s unique, matching the surrounding `canvasProviders` / `CanvasProviderSource` vocabulary. - Rename the declared-invokable surface from "action" to "operation" to match the changeset channel: `SessionCanvasOperation`, `operations`, `canvasInvokeOperation` (`operationName`). Channel state-mutation Actions (`canvas/updated`, `canvas/closeRequested`) stay Actions. - Trim `OpenCanvasRef` to `{ channel, canvasId, title?, availability }`; the channel URI is the sole identity, with full identity on CanvasState. - Remove `canvas/message` and the bespoke `canvasReadResource` / `CanvasResourceContent` content path. Canvas content is resolved over the general symmetrical `resourceRead` command, so a canvas renders over a relay with no direct host connection and no canvas-specific content method. Regenerate all five client mirrors, schema, and reference docs; update the spec/guide prose, reducer fixtures, and changelog fragment. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cd480c0-612c-4854-85cc-20fb91681bae --- clients/go/ahp/reducers.go | 9 +- clients/go/ahptypes/actions.generated.go | 23 --- clients/go/ahptypes/commands.generated.go | 70 ++----- clients/go/ahptypes/state.generated.go | 76 +++---- .../microsoft/agenthostprotocol/Reducers.kt | 9 +- .../generated/Actions.generated.kt | 16 +- .../generated/Commands.generated.kt | 66 ++---- .../generated/Errors.generated.kt | 2 +- .../generated/State.generated.kt | 67 +++--- clients/rust/crates/ahp-types/src/actions.rs | 20 -- clients/rust/crates/ahp-types/src/commands.rs | 79 ++------ clients/rust/crates/ahp-types/src/errors.rs | 2 +- clients/rust/crates/ahp-types/src/state.rs | 79 ++++---- clients/rust/crates/ahp/src/reducers.rs | 9 +- .../Generated/Actions.generated.swift | 19 -- .../Generated/Commands.generated.swift | 91 ++------- .../Generated/Errors.generated.swift | 2 +- .../Generated/State.generated.swift | 83 ++++---- .../Sources/AgentHostProtocol/Reducers.swift | 13 +- docs/.changes/20260709-canvas-channel.json | 2 +- docs/guide/canvases.md | 11 +- docs/specification/canvas-channel.md | 61 +++--- docs/specification/subscriptions.md | 4 +- schema/actions.schema.json | 93 +++------ schema/commands.schema.json | 190 +++++------------- schema/errors.schema.json | 190 +++++------------- schema/notifications.schema.json | 74 +++---- schema/state.schema.json | 74 +++---- scripts/generate-go.ts | 7 +- scripts/generate-kotlin.ts | 8 +- scripts/generate-rust.ts | 9 +- scripts/generate-swift.ts | 8 +- types/action-origin.generated.ts | 4 - types/channels-canvas/actions.ts | 21 -- types/channels-canvas/commands.ts | 116 ++--------- types/channels-canvas/reducer.ts | 10 +- types/channels-canvas/state.ts | 20 +- types/channels-session/state.ts | 62 +++--- types/common/actions.ts | 5 +- types/common/commands.ts | 2 +- types/common/errors.ts | 5 +- types/common/messages.ts | 13 +- types/index.ts | 10 +- ...session-canvaseschanged-sets-registry.json | 14 +- ...on-opencanvaseschanged-sets-catalogue.json | 10 - .../234-canvas-updated-merges-all-fields.json | 8 +- ...nvas-updated-partial-preserves-absent.json | 6 +- ...-canvas-updated-partial-complementary.json | 8 +- .../237-canvas-closerequested-no-op.json | 4 +- .../reducers/238-canvas-message-no-op.json | 26 --- ...9-canvas-unknown-action-type-is-no-op.json | 4 +- types/version/message-checks.ts | 7 +- types/version/registry.ts | 1 - 53 files changed, 544 insertions(+), 1278 deletions(-) delete mode 100644 types/test-cases/reducers/238-canvas-message-no-op.json diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 4b58fee5..e8bac92d 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -1588,10 +1588,9 @@ func ApplyActionToResourceWatch(state *ahptypes.ResourceWatchState, action ahpty // place. `canvas/updated` is a sparse merge — a presented field // (title, status, url, availability) overwrites the corresponding // state field and an absent field preserves the current value. -// `canvas/closeRequested` and `canvas/message` are side-effect-only -// client→host signals the host acts on out of band, so they leave the -// state unchanged. Returns [ReduceOutcomeOutOfScope] for actions that -// target a different state tree. +// `canvas/closeRequested` is a side-effect-only client→host signal the +// host acts on out of band, so it leaves the state unchanged. Returns +// [ReduceOutcomeOutOfScope] for actions that target a different state tree. func ApplyActionToCanvas(state *ahptypes.CanvasState, action ahptypes.StateAction) ReduceOutcome { switch a := action.Value.(type) { case *ahptypes.CanvasUpdatedAction: @@ -1610,8 +1609,6 @@ func ApplyActionToCanvas(state *ahptypes.CanvasState, action ahptypes.StateActio return ReduceOutcomeApplied case *ahptypes.CanvasCloseRequestedAction: return ReduceOutcomeNoOp - case *ahptypes.CanvasMessageAction: - return ReduceOutcomeNoOp } return ReduceOutcomeOutOfScope } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index a45216de..e5555fed 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -102,7 +102,6 @@ const ( ActionTypeSessionOpenCanvasesChanged ActionType = "session/openCanvasesChanged" ActionTypeCanvasUpdated ActionType = "canvas/updated" ActionTypeCanvasCloseRequested ActionType = "canvas/closeRequested" - ActionTypeCanvasMessage ActionType = "canvas/message" ) // ─── Action Envelope ───────────────────────────────────────────────── @@ -1382,21 +1381,6 @@ type CanvasCloseRequestedAction struct { Type ActionType `json:"type"` } -// An opaque message relayed between the rendered canvas View and the -// instance's provider — the relay-carried analogue of a `postMessage` bridge. -// -// Bidirectional: a client dispatches it to carry a View→provider message, and -// the host emits it to carry a provider→View message (routed to the provider -// resolved for the instance, or handled host-internally for a server-side -// provider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it -// is a pure signal with a no-op reducer, so it never bloats channel state. See -// {@link /specification/canvas-channel | Canvas Channel}. -type CanvasMessageAction struct { - Type ActionType `json:"type"` - // Opaque, provider-defined message payload. - Payload json.RawMessage `json:"payload"` -} - // ─── StateAction Union ─────────────────────────────────────────────── // StateAction is the discriminated union of every state action. @@ -1491,7 +1475,6 @@ func (*TerminalCommandFinishedAction) isStateAction() {} func (*ResourceWatchChangedAction) isStateAction() {} func (*CanvasUpdatedAction) isStateAction() {} func (*CanvasCloseRequestedAction) isStateAction() {} -func (*CanvasMessageAction) isStateAction() {} // StateActionUnknown carries an unrecognized StateAction variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. type StateActionUnknown struct { @@ -2005,12 +1988,6 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value - case "canvas/message": - var value CanvasMessageAction - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value default: raw := make(json.RawMessage, len(data)) copy(raw, data) diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index efb18b88..4cbea657 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -176,7 +176,7 @@ type ClientCapabilities struct { McpApps map[string]json.RawMessage `json:"mcpApps,omitempty"` // Client can render canvases and host client-declared canvas providers — it // can render an opaque canvas URL in an isolated surface, and it can answer - // `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + // `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases // it declares via {@link SessionActiveClient.canvasProviders}. // // Hosts SHOULD only populate {@link SessionState.canvases} / @@ -1039,8 +1039,8 @@ type CanvasOpenParams struct { Channel URI `json:"channel"` // Provider-local canvas id to open. CanvasId string `json:"canvasId"` - // Owning provider id. - ExtensionId string `json:"extensionId"` + // Owning provider id (opaque to AHP). + ProviderId string `json:"providerId"` // Caller-minted handle for the new instance. InstanceId string `json:"instanceId"` // Open input, validated by the provider against its declared schema. @@ -1057,30 +1057,30 @@ type CanvasOpenResult struct { Status *string `json:"status,omitempty"` } -// Invokes one of a canvas's declared actions against its provider. +// Invokes one of a canvas's declared operations against its provider. // // Sent by the host to the providing client (or resolved host-internally for a // server-side provider) when the agent invokes a -// {@link SessionCanvasAction | declared action} on an open instance. The +// {@link SessionCanvasOperation | declared operation} on an open instance. The // provider returns an opaque, provider-defined value. Registered symmetrically // with the rest of the provider family (see {@link CanvasOpenParams}). -type CanvasInvokeActionParams struct { +type CanvasInvokeOperationParams struct { // Channel URI this command targets. Channel URI `json:"channel"` - // Instance handle the action targets. + // Instance handle the operation targets. InstanceId string `json:"instanceId"` // Provider-local canvas id of the instance. CanvasId string `json:"canvasId"` - // Owning provider id. - ExtensionId string `json:"extensionId"` - // Declared action name to invoke. - ActionName string `json:"actionName"` - // Action input, validated by the provider against its declared schema. + // Owning provider id (opaque to AHP). + ProviderId string `json:"providerId"` + // Declared operation name to invoke. + OperationName string `json:"operationName"` + // Operation input, validated by the provider against its declared schema. Input map[string]json.RawMessage `json:"input,omitempty"` } -// Result of the `canvasInvokeAction` command. -type CanvasInvokeActionResult struct { +// Result of the `canvasInvokeOperation` command. +type CanvasInvokeOperationResult struct { // Opaque, provider-defined return value. Value *json.RawMessage `json:"value,omitempty"` } @@ -1099,46 +1099,8 @@ type CanvasCloseParams struct { InstanceId string `json:"instanceId"` // Provider-local canvas id of the instance. CanvasId string `json:"canvasId"` - // Owning provider id. - ExtensionId string `json:"extensionId"` -} - -// Reads channel-served canvas content by `ahp-canvas-content:` URI. -// -// A client → host request, modeled on MCP's `resources/read`. When a canvas's -// {@link CanvasState.url} (or a sub-resource the rendered document references) -// is an `ahp-canvas-content://` address, the renderer cannot -// dial the host directly — for example in a relayed deployment behind a broker -// — so it resolves the bytes over the instance's existing `ahp-canvas:/` -// channel with this request instead of loading a network URL. The `` -// segment of the URI identifies which canvas channel to read from. See -// {@link /specification/canvas-channel | Canvas Channel}. -type CanvasReadResourceParams struct { - // Channel URI this command targets. - Channel URI `json:"channel"` - // An `ahp-canvas-content://` content URI to read. - Uri string `json:"uri"` -} - -// Result of the `canvasReadResource` command. -type CanvasReadResourceResult struct { - // The resolved content parts, wrapped for forward compatibility. - Contents []CanvasResourceContent `json:"contents"` -} - -// One resolved piece of channel-served canvas content. -// -// Carries exactly one of {@link text} (text payloads) or {@link blob} -// (base64-encoded binary payloads). -type CanvasResourceContent struct { - // The content URI this part resolves. - Uri string `json:"uri"` - // MIME type of the content, when known. - MimeType *string `json:"mimeType,omitempty"` - // UTF-8 text content, for text payloads. - Text *string `json:"text,omitempty"` - // Base64-encoded content, for binary payloads. - Blob *string `json:"blob,omitempty"` + // Owning provider id (opaque to AHP). + ProviderId string `json:"providerId"` } // ─── ReconnectResult Union ──────────────────────────────────────────── diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 33271e0e..cf417027 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -844,8 +844,8 @@ type SessionActiveClient struct { // {@link SessionState.canvases} with // {@link SessionCanvasDeclaration.source | `source`} set to // `{ kind: 'client', clientId }` and routes - // `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back - // to this client. Only meaningful for a client that declared + // `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them + // back to this client. Only meaningful for a client that declared // {@link ClientCapabilities.canvas}. CanvasProviders []ClientCanvasDeclaration `json:"canvasProviders,omitempty"` } @@ -3232,15 +3232,16 @@ type ResourceChange struct { Type ResourceChangeType `json:"type"` } -// One named action a canvas exposes to the agent, mirroring the shape of a +// One named operation a canvas exposes to the agent, mirroring the shape of a // {@link ToolDefinition} entry. Unique within its owning -// `(extensionId, canvasId)`. -type SessionCanvasAction struct { - // Action name, unique within the owning `(extensionId, canvasId)`. +// `(providerId, canvasId)`. Named "operation" to match the changeset channel's +// operation vocabulary. +type SessionCanvasOperation struct { + // Operation name, unique within the owning `(providerId, canvasId)`. Name string `json:"name"` - // Human-readable description of what the action does. + // Human-readable description of what the operation does. Description *string `json:"description,omitempty"` - // JSON Schema for the action's input. Opaque to AHP; mirrors the + // JSON Schema for the operation's input. Opaque to AHP; mirrors the // {@link ToolDefinition.inputSchema} shape. InputSchema map[string]json.RawMessage `json:"inputSchema,omitempty"` } @@ -3248,11 +3249,11 @@ type SessionCanvasAction struct { // One entry in the aggregated {@link SessionState.canvases} registry — a canvas // the agent can open, contributed by a server-side or client-declared provider. type SessionCanvasDeclaration struct { - // Owning provider id. Stable across declarations and instances. - ExtensionId string `json:"extensionId"` - // Human-readable provider name. - ExtensionName *string `json:"extensionName,omitempty"` - // Provider-local canvas id. Unique within `extensionId`. + // Owning provider id — an opaque namespace string AHP does not interpret, + // carried so a provider-local {@link canvasId} stays unique across providers. + // Stable across declarations and instances. + ProviderId string `json:"providerId"` + // Provider-local canvas id. Unique within `providerId`. CanvasId string `json:"canvasId"` // Human-readable canvas name. DisplayName string `json:"displayName"` @@ -3261,15 +3262,15 @@ type SessionCanvasDeclaration struct { // JSON Schema for the canvas's open input. Opaque to AHP; mirrors the // {@link ToolDefinition.inputSchema} shape. InputSchema map[string]json.RawMessage `json:"inputSchema,omitempty"` - // Actions this canvas exposes to the agent. - Actions []SessionCanvasAction `json:"actions,omitempty"` + // Operations this canvas exposes to the agent. + Operations []SessionCanvasOperation `json:"operations,omitempty"` // Where the declaration came from — for routing and cleanup. Source CanvasProviderSource `json:"source"` } // The lighter declaration shape a client publishes on // {@link SessionActiveClient.canvasProviders}. The host derives the -// `extensionId` and {@link SessionCanvasDeclaration.source | `source`} when +// `providerId` and {@link SessionCanvasDeclaration.source | `source`} when // folding it into {@link SessionState.canvases}. type ClientCanvasDeclaration struct { // Provider-local canvas id, unique within the publishing client. @@ -3280,8 +3281,8 @@ type ClientCanvasDeclaration struct { Description string `json:"description"` // JSON Schema for the canvas's open input. Opaque to AHP. InputSchema map[string]json.RawMessage `json:"inputSchema,omitempty"` - // Actions this canvas exposes to the agent. - Actions []SessionCanvasAction `json:"actions,omitempty"` + // Operations this canvas exposes to the agent. + Operations []SessionCanvasOperation `json:"operations,omitempty"` } // A lightweight catalogue entry for one open canvas instance, surfaced on @@ -3289,18 +3290,17 @@ type ClientCanvasDeclaration struct { // state lives on the instance's own {@link CanvasState} channel; this entry // exists so a subscriber can discover the channel URI and render it without // subscribing to every instance. +// +// The instance is identified solely by its {@link channel} URI — the other +// identity fields (`instanceId`, `providerId`) live on the full +// {@link CanvasState}, not here. type OpenCanvasRef struct { - // Server-assigned instance handle, unique within the session. - InstanceId string `json:"instanceId"` - // The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load - // the full {@link CanvasState}. + // The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the + // open canvas; subscribe to it to load the full {@link CanvasState}. Channel URI `json:"channel"` - // Provider-local canvas id this instance was opened from. + // Provider-local canvas id this instance was opened from. Retained so a + // catalogue view can pick the right native renderer without subscribing. CanvasId string `json:"canvasId"` - // Owning provider id. - ExtensionId string `json:"extensionId"` - // Human-readable provider name. - ExtensionName *string `json:"extensionName,omitempty"` // Current instance title, mirrored from {@link CanvasState.title}. Title *string `json:"title,omitempty"` // Whether the instance's provider is currently available. @@ -3314,8 +3314,8 @@ type CanvasServerProviderSource struct { } // A canvas provided by a connected client. The host routes -// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas -// to the identified client. +// `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this +// canvas to the identified client. type CanvasClientProviderSource struct { Kind CanvasProviderKind `json:"kind"` // `clientId` of the providing client (matches `initialize`). @@ -3333,17 +3333,16 @@ type CanvasClientProviderSource struct { // // Rendering is state-driven: a client renders the canvas by reading // {@link url} and resolving it per the renderer's URL policy — directly for a -// reachable address, or over this channel via `canvasReadResource` for an -// `ahp-canvas-content:` address. It never receives a "render this" request. +// reachable address, or over the general `resourceRead` command for a +// content-reference address. It never receives a "render this" request. type CanvasState struct { // Server-assigned instance handle, unique within the session. InstanceId string `json:"instanceId"` // Provider-local canvas id this instance was opened from. CanvasId string `json:"canvasId"` - // Owning provider id. - ExtensionId string `json:"extensionId"` - // Human-readable provider name. - ExtensionName *string `json:"extensionName,omitempty"` + // Owning provider id — an opaque namespace string AHP does not interpret, + // carried so a provider-local {@link canvasId} stays unique across providers. + ProviderId string `json:"providerId"` // Human-readable canvas name. DisplayName *string `json:"displayName,omitempty"` // Input the agent supplied when opening the instance. Retained so the @@ -3355,8 +3354,11 @@ type CanvasState struct { Status *string `json:"status,omitempty"` // Renderer-targeted address for the opaque canvas content — either a // directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) - // or a channel-served `ahp-canvas-content://` address the - // renderer resolves over this channel with `canvasReadResource`. The + // or a content-reference URI the renderer resolves with the general + // `resourceRead` command. Resolving over `resourceRead` keeps content flowing + // entirely over the AHP transport, so a relayed or brokered deployment — + // where the host is reachable only over AHP and cannot be dialed directly — + // can still serve every byte, with no port or direct connection required. The // renderer dispatches on the scheme and enforces its URL policy. See // {@link /specification/canvas-channel | Canvas Channel}. Url *string `json:"url,omitempty"` 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 26e1e316..03c0c99d 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -1559,10 +1559,10 @@ public fun resourceWatchReducer(state: ResourceWatchState, action: StateAction): * Pure reducer for a [CanvasState]. `canvas/updated` is a sparse merge — * a presented field (title, status, url, availability) overwrites the * corresponding [CanvasState] field while an absent field preserves the - * current value. `canvas/closeRequested` and `canvas/message` are pure - * client→host signals the host acts on out of band, mirroring how - * `terminal/input` is side-effect-only, so they return [state] unchanged. - * Actions belonging to other channels (or unknown variants) are no-ops. + * current value. `canvas/closeRequested` is a pure client→host signal the + * host acts on out of band, mirroring how `terminal/input` is + * side-effect-only, so it returns [state] unchanged. Actions belonging to + * other channels (or unknown variants) are no-ops. */ public fun canvasReducer(state: CanvasState, action: StateAction): CanvasState = when (action) { is StateActionCanvasUpdated -> state.copy( @@ -1572,6 +1572,5 @@ public fun canvasReducer(state: CanvasState, action: StateAction): CanvasState = availability = action.value.availability ?: state.availability, ) is StateActionCanvasCloseRequested -> state - is StateActionCanvasMessage -> state else -> state } 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 95269b6c..7cccd8a3 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 @@ -191,9 +191,7 @@ enum class ActionType { @SerialName("canvas/updated") CANVAS_UPDATED, @SerialName("canvas/closeRequested") - CANVAS_CLOSE_REQUESTED, - @SerialName("canvas/message") - CANVAS_MESSAGE + CANVAS_CLOSE_REQUESTED } // ─── Action Infrastructure ────────────────────────────────────────────────── @@ -1397,15 +1395,6 @@ data class CanvasCloseRequestedAction( val type: ActionType ) -@Serializable -data class CanvasMessageAction( - val type: ActionType, - /** - * Opaque, provider-defined message payload. - */ - val payload: JsonElement -) - // ─── Partial Summary Types ────────────────────────────────────────────────── @Serializable @@ -1549,7 +1538,6 @@ sealed interface StateAction @JvmInline value class StateActionResourceWatchChanged(val value: ResourceWatchChangedAction) : StateAction @JvmInline value class StateActionCanvasUpdated(val value: CanvasUpdatedAction) : StateAction @JvmInline value class StateActionCanvasCloseRequested(val value: CanvasCloseRequestedAction) : StateAction -@JvmInline value class StateActionCanvasMessage(val value: CanvasMessageAction) : StateAction @JvmInline value class StateActionUnknown(val raw: JsonObject) : StateAction internal object StateActionSerializer : KSerializer { @@ -1648,7 +1636,6 @@ internal object StateActionSerializer : KSerializer { "resourceWatch/changed" -> StateActionResourceWatchChanged(input.json.decodeFromJsonElement(ResourceWatchChangedAction.serializer(), element)) "canvas/updated" -> StateActionCanvasUpdated(input.json.decodeFromJsonElement(CanvasUpdatedAction.serializer(), element)) "canvas/closeRequested" -> StateActionCanvasCloseRequested(input.json.decodeFromJsonElement(CanvasCloseRequestedAction.serializer(), element)) - "canvas/message" -> StateActionCanvasMessage(input.json.decodeFromJsonElement(CanvasMessageAction.serializer(), element)) else -> StateActionUnknown(obj) } } @@ -1740,7 +1727,6 @@ internal object StateActionSerializer : KSerializer { is StateActionResourceWatchChanged -> output.json.encodeToJsonElement(ResourceWatchChangedAction.serializer(), value.value) is StateActionCanvasUpdated -> output.json.encodeToJsonElement(CanvasUpdatedAction.serializer(), value.value) is StateActionCanvasCloseRequested -> output.json.encodeToJsonElement(CanvasCloseRequestedAction.serializer(), value.value) - is StateActionCanvasMessage -> output.json.encodeToJsonElement(CanvasMessageAction.serializer(), value.value) is StateActionUnknown -> value.raw } output.encodeJsonElement(element) 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 38d62026..15e16878 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 @@ -222,7 +222,7 @@ data class ClientCapabilities( /** * Client can render canvases and host client-declared canvas providers — it * can render an opaque canvas URL in an isolated surface, and it can answer - * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + * `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases * it declares via {@link SessionActiveClient.canvasProviders}. * * Hosts SHOULD only populate {@link SessionState.canvases} / @@ -1194,9 +1194,9 @@ data class CanvasOpenParams( */ val canvasId: String, /** - * Owning provider id. + * Owning provider id (opaque to AHP). */ - val extensionId: String, + val providerId: String, /** * Caller-minted handle for the new instance. */ @@ -1224,13 +1224,13 @@ data class CanvasOpenResult( ) @Serializable -data class CanvasInvokeActionParams( +data class CanvasInvokeOperationParams( /** * Channel URI this command targets. */ val channel: String, /** - * Instance handle the action targets. + * Instance handle the operation targets. */ val instanceId: String, /** @@ -1238,21 +1238,21 @@ data class CanvasInvokeActionParams( */ val canvasId: String, /** - * Owning provider id. + * Owning provider id (opaque to AHP). */ - val extensionId: String, + val providerId: String, /** - * Declared action name to invoke. + * Declared operation name to invoke. */ - val actionName: String, + val operationName: String, /** - * Action input, validated by the provider against its declared schema. + * Operation input, validated by the provider against its declared schema. */ val input: Map? = null ) @Serializable -data class CanvasInvokeActionResult( +data class CanvasInvokeOperationResult( /** * Opaque, provider-defined return value. */ @@ -1274,49 +1274,9 @@ data class CanvasCloseParams( */ val canvasId: String, /** - * Owning provider id. + * Owning provider id (opaque to AHP). */ - val extensionId: String -) - -@Serializable -data class CanvasReadResourceParams( - /** - * Channel URI this command targets. - */ - val channel: String, - /** - * An `ahp-canvas-content://` content URI to read. - */ - val uri: String -) - -@Serializable -data class CanvasReadResourceResult( - /** - * The resolved content parts, wrapped for forward compatibility. - */ - val contents: List -) - -@Serializable -data class CanvasResourceContent( - /** - * The content URI this part resolves. - */ - val uri: String, - /** - * MIME type of the content, when known. - */ - val mimeType: String? = null, - /** - * UTF-8 text content, for text payloads. - */ - val text: String? = null, - /** - * Base64-encoded content, for binary payloads. - */ - val blob: String? = null + val providerId: String ) // ─── ReconnectResult Union ────────────────────────────────────────────────── diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt index 8d9e0e43..f6423b89 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt @@ -59,7 +59,7 @@ object AhpErrorCodes { const val ALREADY_EXISTS: Int = -32010 /** An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state */ const val CONFLICT: Int = -32011 - /** A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` */ + /** A canvas provider request (canvasOpen, canvasInvokeOperation, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` */ const val CANVAS_PROVIDER_ERROR: Int = -32012 } 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 e089c127..3e549f68 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 @@ -1446,8 +1446,8 @@ data class SessionActiveClient( * {@link SessionState.canvases} with * {@link SessionCanvasDeclaration.source | `source`} set to * `{ kind: 'client', clientId }` and routes - * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back - * to this client. Only meaningful for a client that declared + * `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them + * back to this client. Only meaningful for a client that declared * {@link ClientCapabilities.canvas}. */ val canvasProviders: List? = null @@ -4341,17 +4341,17 @@ data class ResourceChange( ) @Serializable -data class SessionCanvasAction( +data class SessionCanvasOperation( /** - * Action name, unique within the owning `(extensionId, canvasId)`. + * Operation name, unique within the owning `(providerId, canvasId)`. */ val name: String, /** - * Human-readable description of what the action does. + * Human-readable description of what the operation does. */ val description: String? = null, /** - * JSON Schema for the action's input. Opaque to AHP; mirrors the + * JSON Schema for the operation's input. Opaque to AHP; mirrors the * {@link ToolDefinition.inputSchema} shape. */ val inputSchema: Map? = null @@ -4360,15 +4360,13 @@ data class SessionCanvasAction( @Serializable data class SessionCanvasDeclaration( /** - * Owning provider id. Stable across declarations and instances. + * Owning provider id — an opaque namespace string AHP does not interpret, + * carried so a provider-local {@link canvasId} stays unique across providers. + * Stable across declarations and instances. */ - val extensionId: String, + val providerId: String, /** - * Human-readable provider name. - */ - val extensionName: String? = null, - /** - * Provider-local canvas id. Unique within `extensionId`. + * Provider-local canvas id. Unique within `providerId`. */ val canvasId: String, /** @@ -4385,9 +4383,9 @@ data class SessionCanvasDeclaration( */ val inputSchema: Map? = null, /** - * Actions this canvas exposes to the agent. + * Operations this canvas exposes to the agent. */ - val actions: List? = null, + val operations: List? = null, /** * Where the declaration came from — for routing and cleanup. */ @@ -4413,34 +4411,23 @@ data class ClientCanvasDeclaration( */ val inputSchema: Map? = null, /** - * Actions this canvas exposes to the agent. + * Operations this canvas exposes to the agent. */ - val actions: List? = null + val operations: List? = null ) @Serializable data class OpenCanvasRef( /** - * Server-assigned instance handle, unique within the session. - */ - val instanceId: String, - /** - * The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load - * the full {@link CanvasState}. + * The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the + * open canvas; subscribe to it to load the full {@link CanvasState}. */ val channel: String, /** - * Provider-local canvas id this instance was opened from. + * Provider-local canvas id this instance was opened from. Retained so a + * catalogue view can pick the right native renderer without subscribing. */ val canvasId: String, - /** - * Owning provider id. - */ - val extensionId: String, - /** - * Human-readable provider name. - */ - val extensionName: String? = null, /** * Current instance title, mirrored from {@link CanvasState.title}. */ @@ -4476,13 +4463,10 @@ data class CanvasState( */ val canvasId: String, /** - * Owning provider id. - */ - val extensionId: String, - /** - * Human-readable provider name. + * Owning provider id — an opaque namespace string AHP does not interpret, + * carried so a provider-local {@link canvasId} stays unique across providers. */ - val extensionName: String? = null, + val providerId: String, /** * Human-readable canvas name. */ @@ -4503,8 +4487,11 @@ data class CanvasState( /** * Renderer-targeted address for the opaque canvas content — either a * directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) - * or a channel-served `ahp-canvas-content://` address the - * renderer resolves over this channel with `canvasReadResource`. The + * or a content-reference URI the renderer resolves with the general + * `resourceRead` command. Resolving over `resourceRead` keeps content flowing + * entirely over the AHP transport, so a relayed or brokered deployment — + * where the host is reachable only over AHP and cannot be dialed directly — + * can still serve every byte, with no port or direct connection required. The * renderer dispatches on the scheme and enforces its URL policy. See * {@link /specification/canvas-channel | Canvas Channel}. */ diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 654424ae..ddda31ac 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -194,8 +194,6 @@ pub enum ActionType { CanvasUpdated, #[serde(rename = "canvas/closeRequested")] CanvasCloseRequested, - #[serde(rename = "canvas/message")] - CanvasMessage, } // ─── Action Envelope ───────────────────────────────────────────────── @@ -1632,22 +1630,6 @@ pub struct CanvasUpdatedAction { #[serde(rename_all = "camelCase")] pub struct CanvasCloseRequestedAction {} -/// An opaque message relayed between the rendered canvas View and the -/// instance's provider — the relay-carried analogue of a `postMessage` bridge. -/// -/// Bidirectional: a client dispatches it to carry a View→provider message, and -/// the host emits it to carry a provider→View message (routed to the provider -/// resolved for the instance, or handled host-internally for a server-side -/// provider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it -/// is a pure signal with a no-op reducer, so it never bloats channel state. See -/// {@link /specification/canvas-channel | Canvas Channel}. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CanvasMessageAction { - /// Opaque, provider-defined message payload. - pub payload: AnyValue, -} - // ─── Partial Summaries ──────────────────────────────────────────────── /// Partial equivalent of ChatSummary — every field is optional for delta updates. @@ -1860,8 +1842,6 @@ pub enum StateAction { CanvasUpdated(CanvasUpdatedAction), #[serde(rename = "canvas/closeRequested")] CanvasCloseRequested(CanvasCloseRequestedAction), - #[serde(rename = "canvas/message")] - CanvasMessage(CanvasMessageAction), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 3d965a8e..0863e868 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -205,7 +205,7 @@ pub struct ClientCapabilities { pub mcp_apps: Option, /// Client can render canvases and host client-declared canvas providers — it /// can render an opaque canvas URL in an isolated surface, and it can answer - /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + /// `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases /// it declares via {@link SessionActiveClient.canvasProviders}. /// /// Hosts SHOULD only populate {@link SessionState.canvases} / @@ -1262,8 +1262,8 @@ pub struct CanvasOpenParams { pub channel: Uri, /// Provider-local canvas id to open. pub canvas_id: String, - /// Owning provider id. - pub extension_id: String, + /// Owning provider id (opaque to AHP). + pub provider_id: String, /// Caller-minted handle for the new instance. pub instance_id: String, /// Open input, validated by the provider against its declared schema. @@ -1286,35 +1286,35 @@ pub struct CanvasOpenResult { pub status: Option, } -/// Invokes one of a canvas's declared actions against its provider. +/// Invokes one of a canvas's declared operations against its provider. /// /// Sent by the host to the providing client (or resolved host-internally for a /// server-side provider) when the agent invokes a -/// {@link SessionCanvasAction | declared action} on an open instance. The +/// {@link SessionCanvasOperation | declared operation} on an open instance. The /// provider returns an opaque, provider-defined value. Registered symmetrically /// with the rest of the provider family (see {@link CanvasOpenParams}). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CanvasInvokeActionParams { +pub struct CanvasInvokeOperationParams { /// Channel URI this command targets. pub channel: Uri, - /// Instance handle the action targets. + /// Instance handle the operation targets. pub instance_id: String, /// Provider-local canvas id of the instance. pub canvas_id: String, - /// Owning provider id. - pub extension_id: String, - /// Declared action name to invoke. - pub action_name: String, - /// Action input, validated by the provider against its declared schema. + /// Owning provider id (opaque to AHP). + pub provider_id: String, + /// Declared operation name to invoke. + pub operation_name: String, + /// Operation input, validated by the provider against its declared schema. #[serde(default, skip_serializing_if = "Option::is_none")] pub input: Option, } -/// Result of the `canvasInvokeAction` command. +/// Result of the `canvasInvokeOperation` command. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub struct CanvasInvokeActionResult { +pub struct CanvasInvokeOperationResult { /// Opaque, provider-defined return value. #[serde(default, skip_serializing_if = "Option::is_none")] pub value: Option, @@ -1336,55 +1336,8 @@ pub struct CanvasCloseParams { pub instance_id: String, /// Provider-local canvas id of the instance. pub canvas_id: String, - /// Owning provider id. - pub extension_id: String, -} - -/// Reads channel-served canvas content by `ahp-canvas-content:` URI. -/// -/// A client → host request, modeled on MCP's `resources/read`. When a canvas's -/// {@link CanvasState.url} (or a sub-resource the rendered document references) -/// is an `ahp-canvas-content://` address, the renderer cannot -/// dial the host directly — for example in a relayed deployment behind a broker -/// — so it resolves the bytes over the instance's existing `ahp-canvas:/` -/// channel with this request instead of loading a network URL. The `` -/// segment of the URI identifies which canvas channel to read from. See -/// {@link /specification/canvas-channel | Canvas Channel}. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CanvasReadResourceParams { - /// Channel URI this command targets. - pub channel: Uri, - /// An `ahp-canvas-content://` content URI to read. - pub uri: String, -} - -/// Result of the `canvasReadResource` command. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CanvasReadResourceResult { - /// The resolved content parts, wrapped for forward compatibility. - pub contents: Vec, -} - -/// One resolved piece of channel-served canvas content. -/// -/// Carries exactly one of {@link text} (text payloads) or {@link blob} -/// (base64-encoded binary payloads). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CanvasResourceContent { - /// The content URI this part resolves. - pub uri: String, - /// MIME type of the content, when known. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - /// UTF-8 text content, for text payloads. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub text: Option, - /// Base64-encoded content, for binary payloads. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub blob: Option, + /// Owning provider id (opaque to AHP). + pub provider_id: String, } // ─── ReconnectResult Union ──────────────────────────────────────────── diff --git a/clients/rust/crates/ahp-types/src/errors.rs b/clients/rust/crates/ahp-types/src/errors.rs index 2351e68b..bcbf470e 100644 --- a/clients/rust/crates/ahp-types/src/errors.rs +++ b/clients/rust/crates/ahp-types/src/errors.rs @@ -55,7 +55,7 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. `ResourceWriteParams.if_match`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; - /// A canvas provider request (`canvasOpen`, `canvasInvokeAction`, or `canvasClose`) failed; the error `data` carries a provider-defined `{ code, message }`. + /// A canvas provider request (`canvasOpen`, `canvasInvokeOperation`, or `canvasClose`) failed; the error `data` carries a provider-defined `{ code, message }`. pub const CANVAS_PROVIDER_ERROR: i32 = -32012; } diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 79e47863..33027ff8 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1248,8 +1248,8 @@ pub struct SessionActiveClient { /// {@link SessionState.canvases} with /// {@link SessionCanvasDeclaration.source | `source`} set to /// `{ kind: 'client', clientId }` and routes - /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back - /// to this client. Only meaningful for a client that declared + /// `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them + /// back to this client. Only meaningful for a client that declared /// {@link ClientCapabilities.canvas}. #[serde(default, skip_serializing_if = "Option::is_none")] pub canvas_providers: Option>, @@ -3903,18 +3903,19 @@ pub struct ResourceChange { pub r#type: ResourceChangeType, } -/// One named action a canvas exposes to the agent, mirroring the shape of a +/// One named operation a canvas exposes to the agent, mirroring the shape of a /// {@link ToolDefinition} entry. Unique within its owning -/// `(extensionId, canvasId)`. +/// `(providerId, canvasId)`. Named "operation" to match the changeset channel's +/// operation vocabulary. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionCanvasAction { - /// Action name, unique within the owning `(extensionId, canvasId)`. +pub struct SessionCanvasOperation { + /// Operation name, unique within the owning `(providerId, canvasId)`. pub name: String, - /// Human-readable description of what the action does. + /// Human-readable description of what the operation does. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - /// JSON Schema for the action's input. Opaque to AHP; mirrors the + /// JSON Schema for the operation's input. Opaque to AHP; mirrors the /// {@link ToolDefinition.inputSchema} shape. #[serde(default, skip_serializing_if = "Option::is_none")] pub input_schema: Option, @@ -3925,12 +3926,11 @@ pub struct SessionCanvasAction { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionCanvasDeclaration { - /// Owning provider id. Stable across declarations and instances. - pub extension_id: String, - /// Human-readable provider name. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub extension_name: Option, - /// Provider-local canvas id. Unique within `extensionId`. + /// Owning provider id — an opaque namespace string AHP does not interpret, + /// carried so a provider-local {@link canvasId} stays unique across providers. + /// Stable across declarations and instances. + pub provider_id: String, + /// Provider-local canvas id. Unique within `providerId`. pub canvas_id: String, /// Human-readable canvas name. pub display_name: String, @@ -3940,16 +3940,16 @@ pub struct SessionCanvasDeclaration { /// {@link ToolDefinition.inputSchema} shape. #[serde(default, skip_serializing_if = "Option::is_none")] pub input_schema: Option, - /// Actions this canvas exposes to the agent. + /// Operations this canvas exposes to the agent. #[serde(default, skip_serializing_if = "Option::is_none")] - pub actions: Option>, + pub operations: Option>, /// Where the declaration came from — for routing and cleanup. pub source: CanvasProviderSource, } /// The lighter declaration shape a client publishes on /// {@link SessionActiveClient.canvasProviders}. The host derives the -/// `extensionId` and {@link SessionCanvasDeclaration.source | `source`} when +/// `providerId` and {@link SessionCanvasDeclaration.source | `source`} when /// folding it into {@link SessionState.canvases}. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -3963,9 +3963,9 @@ pub struct ClientCanvasDeclaration { /// JSON Schema for the canvas's open input. Opaque to AHP. #[serde(default, skip_serializing_if = "Option::is_none")] pub input_schema: Option, - /// Actions this canvas exposes to the agent. + /// Operations this canvas exposes to the agent. #[serde(default, skip_serializing_if = "Option::is_none")] - pub actions: Option>, + pub operations: Option>, } /// A lightweight catalogue entry for one open canvas instance, surfaced on @@ -3973,21 +3973,19 @@ pub struct ClientCanvasDeclaration { /// state lives on the instance's own {@link CanvasState} channel; this entry /// exists so a subscriber can discover the channel URI and render it without /// subscribing to every instance. +/// +/// The instance is identified solely by its {@link channel} URI — the other +/// identity fields (`instanceId`, `providerId`) live on the full +/// {@link CanvasState}, not here. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenCanvasRef { - /// Server-assigned instance handle, unique within the session. - pub instance_id: String, - /// The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load - /// the full {@link CanvasState}. + /// The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the + /// open canvas; subscribe to it to load the full {@link CanvasState}. pub channel: Uri, - /// Provider-local canvas id this instance was opened from. + /// Provider-local canvas id this instance was opened from. Retained so a + /// catalogue view can pick the right native renderer without subscribing. pub canvas_id: String, - /// Owning provider id. - pub extension_id: String, - /// Human-readable provider name. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub extension_name: Option, /// Current instance title, mirrored from {@link CanvasState.title}. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, @@ -4002,8 +4000,8 @@ pub struct OpenCanvasRef { pub struct CanvasServerProviderSource {} /// A canvas provided by a connected client. The host routes -/// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas -/// to the identified client. +/// `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this +/// canvas to the identified client. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CanvasClientProviderSource { @@ -4022,8 +4020,8 @@ pub struct CanvasClientProviderSource { /// /// Rendering is state-driven: a client renders the canvas by reading /// {@link url} and resolving it per the renderer's URL policy — directly for a -/// reachable address, or over this channel via `canvasReadResource` for an -/// `ahp-canvas-content:` address. It never receives a "render this" request. +/// reachable address, or over the general `resourceRead` command for a +/// content-reference address. It never receives a "render this" request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CanvasState { @@ -4031,11 +4029,9 @@ pub struct CanvasState { pub instance_id: String, /// Provider-local canvas id this instance was opened from. pub canvas_id: String, - /// Owning provider id. - pub extension_id: String, - /// Human-readable provider name. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub extension_name: Option, + /// Owning provider id — an opaque namespace string AHP does not interpret, + /// carried so a provider-local {@link canvasId} stays unique across providers. + pub provider_id: String, /// Human-readable canvas name. #[serde(default, skip_serializing_if = "Option::is_none")] pub display_name: Option, @@ -4051,8 +4047,11 @@ pub struct CanvasState { pub status: Option, /// Renderer-targeted address for the opaque canvas content — either a /// directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) - /// or a channel-served `ahp-canvas-content://` address the - /// renderer resolves over this channel with `canvasReadResource`. The + /// or a content-reference URI the renderer resolves with the general + /// `resourceRead` command. Resolving over `resourceRead` keeps content flowing + /// entirely over the AHP transport, so a relayed or brokered deployment — + /// where the host is reachable only over AHP and cannot be dialed directly — + /// can still serve every byte, with no port or direct connection required. The /// renderer dispatches on the scheme and enforces its URL policy. See /// {@link /specification/canvas-channel | Canvas Channel}. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 8d85f098..59fd3ac9 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1713,10 +1713,10 @@ pub fn apply_action_to_resource_watch( /// /// `canvas/updated` is a sparse merge — each present field overwrites the /// corresponding [`CanvasState`] field and an absent field preserves the -/// current value. `canvas/closeRequested` and `canvas/message` are pure -/// signals with no state effect (the host acts on them out of band), -/// mirroring how `terminal/input` is side-effect-only. Actions targeting a -/// different scope short-circuit as [`ReduceOutcome::OutOfScope`]. +/// current value. `canvas/closeRequested` is a pure signal with no state +/// effect (the host acts on it out of band), mirroring how `terminal/input` +/// is side-effect-only. Actions targeting a different scope short-circuit as +/// [`ReduceOutcome::OutOfScope`]. pub fn apply_action_to_canvas(state: &mut CanvasState, action: &StateAction) -> ReduceOutcome { match action { StateAction::CanvasUpdated(a) => { @@ -1735,7 +1735,6 @@ pub fn apply_action_to_canvas(state: &mut CanvasState, action: &StateAction) -> ReduceOutcome::Applied } StateAction::CanvasCloseRequested(_) => ReduceOutcome::NoOp, - StateAction::CanvasMessage(_) => ReduceOutcome::NoOp, _ => ReduceOutcome::OutOfScope, } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 0c880d36..dafdacb6 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -89,7 +89,6 @@ public enum ActionType: String, Codable, Sendable { case sessionOpenCanvasesChanged = "session/openCanvasesChanged" case canvasUpdated = "canvas/updated" case canvasCloseRequested = "canvas/closeRequested" - case canvasMessage = "canvas/message" } // MARK: - Action Infrastructure @@ -1855,20 +1854,6 @@ public struct CanvasCloseRequestedAction: Codable, Sendable { } } -public struct CanvasMessageAction: Codable, Sendable { - public var type: ActionType - /// Opaque, provider-defined message payload. - public var payload: AnyCodable - - public init( - type: ActionType, - payload: AnyCodable - ) { - self.type = type - self.payload = payload - } -} - // MARK: - Partial Summary Types public struct PartialChatSummary: Codable, Sendable { @@ -2005,7 +1990,6 @@ public enum StateAction: Codable, Sendable { case resourceWatchChanged(ResourceWatchChangedAction) case canvasUpdated(CanvasUpdatedAction) case canvasCloseRequested(CanvasCloseRequestedAction) - case canvasMessage(CanvasMessageAction) /// Unknown or future action type; reducers treat this as a no-op. /// The raw payload (including its `type` discriminant) is preserved /// as an `AnyCodable` so a decode→encode round-trip re-emits it @@ -2184,8 +2168,6 @@ public enum StateAction: Codable, Sendable { self = .canvasUpdated(try CanvasUpdatedAction(from: decoder)) case "canvas/closeRequested": self = .canvasCloseRequested(try CanvasCloseRequestedAction(from: decoder)) - case "canvas/message": - self = .canvasMessage(try CanvasMessageAction(from: decoder)) default: self = .unknown(try AnyCodable(from: decoder)) } @@ -2276,7 +2258,6 @@ public enum StateAction: Codable, Sendable { case .resourceWatchChanged(let v): try v.encode(to: encoder) case .canvasUpdated(let v): try v.encode(to: encoder) case .canvasCloseRequested(let v): try v.encode(to: encoder) - case .canvasMessage(let v): try v.encode(to: encoder) case .unknown(let value): try value.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 7c183b76..cf416367 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -179,7 +179,7 @@ public struct ClientCapabilities: Codable, Sendable { public var mcpApps: [String: AnyCodable]? /// Client can render canvases and host client-declared canvas providers — it /// can render an opaque canvas URL in an isolated surface, and it can answer - /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + /// `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases /// it declares via {@link SessionActiveClient.canvasProviders}. /// /// Hosts SHOULD only populate {@link SessionState.canvases} / @@ -1361,8 +1361,8 @@ public struct CanvasOpenParams: Codable, Sendable { public var channel: String /// Provider-local canvas id to open. public var canvasId: String - /// Owning provider id. - public var extensionId: String + /// Owning provider id (opaque to AHP). + public var providerId: String /// Caller-minted handle for the new instance. public var instanceId: String /// Open input, validated by the provider against its declared schema. @@ -1371,13 +1371,13 @@ public struct CanvasOpenParams: Codable, Sendable { public init( channel: String, canvasId: String, - extensionId: String, + providerId: String, instanceId: String, input: [String: AnyCodable]? = nil ) { self.channel = channel self.canvasId = canvasId - self.extensionId = extensionId + self.providerId = providerId self.instanceId = instanceId self.input = input } @@ -1402,38 +1402,38 @@ public struct CanvasOpenResult: Codable, Sendable { } } -public struct CanvasInvokeActionParams: Codable, Sendable { +public struct CanvasInvokeOperationParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String - /// Instance handle the action targets. + /// Instance handle the operation targets. public var instanceId: String /// Provider-local canvas id of the instance. public var canvasId: String - /// Owning provider id. - public var extensionId: String - /// Declared action name to invoke. - public var actionName: String - /// Action input, validated by the provider against its declared schema. + /// Owning provider id (opaque to AHP). + public var providerId: String + /// Declared operation name to invoke. + public var operationName: String + /// Operation input, validated by the provider against its declared schema. public var input: [String: AnyCodable]? public init( channel: String, instanceId: String, canvasId: String, - extensionId: String, - actionName: String, + providerId: String, + operationName: String, input: [String: AnyCodable]? = nil ) { self.channel = channel self.instanceId = instanceId self.canvasId = canvasId - self.extensionId = extensionId - self.actionName = actionName + self.providerId = providerId + self.operationName = operationName self.input = input } } -public struct CanvasInvokeActionResult: Codable, Sendable { +public struct CanvasInvokeOperationResult: Codable, Sendable { /// Opaque, provider-defined return value. public var value: AnyCodable? @@ -1451,68 +1451,19 @@ public struct CanvasCloseParams: Codable, Sendable { public var instanceId: String /// Provider-local canvas id of the instance. public var canvasId: String - /// Owning provider id. - public var extensionId: String + /// Owning provider id (opaque to AHP). + public var providerId: String public init( channel: String, instanceId: String, canvasId: String, - extensionId: String + providerId: String ) { self.channel = channel self.instanceId = instanceId self.canvasId = canvasId - self.extensionId = extensionId - } -} - -public struct CanvasReadResourceParams: Codable, Sendable { - /// Channel URI this command targets. - public var channel: String - /// An `ahp-canvas-content://` content URI to read. - public var uri: String - - public init( - channel: String, - uri: String - ) { - self.channel = channel - self.uri = uri - } -} - -public struct CanvasReadResourceResult: Codable, Sendable { - /// The resolved content parts, wrapped for forward compatibility. - public var contents: [CanvasResourceContent] - - public init( - contents: [CanvasResourceContent] - ) { - self.contents = contents - } -} - -public struct CanvasResourceContent: Codable, Sendable { - /// The content URI this part resolves. - public var uri: String - /// MIME type of the content, when known. - public var mimeType: String? - /// UTF-8 text content, for text payloads. - public var text: String? - /// Base64-encoded content, for binary payloads. - public var blob: String? - - public init( - uri: String, - mimeType: String? = nil, - text: String? = nil, - blob: String? = nil - ) { - self.uri = uri - self.mimeType = mimeType - self.text = text - self.blob = blob + self.providerId = providerId } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift index 7ee5cc69..4ee14031 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift @@ -42,7 +42,7 @@ public enum AhpErrorCodes { public static let alreadyExists = -32010 /// An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state public static let conflict = -32011 - /// A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` + /// A canvas provider request (canvasOpen, canvasInvokeOperation, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` public static let canvasProviderError = -32012 } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index c60702ad..6a48123b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1215,8 +1215,8 @@ public struct SessionActiveClient: Codable, Sendable { /// {@link SessionState.canvases} with /// {@link SessionCanvasDeclaration.source | `source`} set to /// `{ kind: 'client', clientId }` and routes - /// `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back - /// to this client. Only meaningful for a client that declared + /// `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them + /// back to this client. Only meaningful for a client that declared /// {@link ClientCapabilities.canvas}. public var canvasProviders: [ClientCanvasDeclaration]? @@ -4843,12 +4843,12 @@ public struct ResourceChange: Codable, Sendable { } } -public struct SessionCanvasAction: Codable, Sendable { - /// Action name, unique within the owning `(extensionId, canvasId)`. +public struct SessionCanvasOperation: Codable, Sendable { + /// Operation name, unique within the owning `(providerId, canvasId)`. public var name: String - /// Human-readable description of what the action does. + /// Human-readable description of what the operation does. public var description: String? - /// JSON Schema for the action's input. Opaque to AHP; mirrors the + /// JSON Schema for the operation's input. Opaque to AHP; mirrors the /// {@link ToolDefinition.inputSchema} shape. public var inputSchema: [String: AnyCodable]? @@ -4864,11 +4864,11 @@ public struct SessionCanvasAction: Codable, Sendable { } public struct SessionCanvasDeclaration: Codable, Sendable { - /// Owning provider id. Stable across declarations and instances. - public var extensionId: String - /// Human-readable provider name. - public var extensionName: String? - /// Provider-local canvas id. Unique within `extensionId`. + /// Owning provider id — an opaque namespace string AHP does not interpret, + /// carried so a provider-local {@link canvasId} stays unique across providers. + /// Stable across declarations and instances. + public var providerId: String + /// Provider-local canvas id. Unique within `providerId`. public var canvasId: String /// Human-readable canvas name. public var displayName: String @@ -4877,28 +4877,26 @@ public struct SessionCanvasDeclaration: Codable, Sendable { /// JSON Schema for the canvas's open input. Opaque to AHP; mirrors the /// {@link ToolDefinition.inputSchema} shape. public var inputSchema: [String: AnyCodable]? - /// Actions this canvas exposes to the agent. - public var actions: [SessionCanvasAction]? + /// Operations this canvas exposes to the agent. + public var operations: [SessionCanvasOperation]? /// Where the declaration came from — for routing and cleanup. public var source: CanvasProviderSource public init( - extensionId: String, - extensionName: String? = nil, + providerId: String, canvasId: String, displayName: String, description: String, inputSchema: [String: AnyCodable]? = nil, - actions: [SessionCanvasAction]? = nil, + operations: [SessionCanvasOperation]? = nil, source: CanvasProviderSource ) { - self.extensionId = extensionId - self.extensionName = extensionName + self.providerId = providerId self.canvasId = canvasId self.displayName = displayName self.description = description self.inputSchema = inputSchema - self.actions = actions + self.operations = operations self.source = source } } @@ -4912,55 +4910,44 @@ public struct ClientCanvasDeclaration: Codable, Sendable { public var description: String /// JSON Schema for the canvas's open input. Opaque to AHP. public var inputSchema: [String: AnyCodable]? - /// Actions this canvas exposes to the agent. - public var actions: [SessionCanvasAction]? + /// Operations this canvas exposes to the agent. + public var operations: [SessionCanvasOperation]? public init( canvasId: String, displayName: String, description: String, inputSchema: [String: AnyCodable]? = nil, - actions: [SessionCanvasAction]? = nil + operations: [SessionCanvasOperation]? = nil ) { self.canvasId = canvasId self.displayName = displayName self.description = description self.inputSchema = inputSchema - self.actions = actions + self.operations = operations } } public struct OpenCanvasRef: Codable, Sendable { - /// Server-assigned instance handle, unique within the session. - public var instanceId: String - /// The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load - /// the full {@link CanvasState}. + /// The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the + /// open canvas; subscribe to it to load the full {@link CanvasState}. public var channel: String - /// Provider-local canvas id this instance was opened from. + /// Provider-local canvas id this instance was opened from. Retained so a + /// catalogue view can pick the right native renderer without subscribing. public var canvasId: String - /// Owning provider id. - public var extensionId: String - /// Human-readable provider name. - public var extensionName: String? /// Current instance title, mirrored from {@link CanvasState.title}. public var title: String? /// Whether the instance's provider is currently available. public var availability: CanvasAvailability public init( - instanceId: String, channel: String, canvasId: String, - extensionId: String, - extensionName: String? = nil, title: String? = nil, availability: CanvasAvailability ) { - self.instanceId = instanceId self.channel = channel self.canvasId = canvasId - self.extensionId = extensionId - self.extensionName = extensionName self.title = title self.availability = availability } @@ -4995,10 +4982,9 @@ public struct CanvasState: Codable, Sendable { public var instanceId: String /// Provider-local canvas id this instance was opened from. public var canvasId: String - /// Owning provider id. - public var extensionId: String - /// Human-readable provider name. - public var extensionName: String? + /// Owning provider id — an opaque namespace string AHP does not interpret, + /// carried so a provider-local {@link canvasId} stays unique across providers. + public var providerId: String /// Human-readable canvas name. public var displayName: String? /// Input the agent supplied when opening the instance. Retained so the @@ -5010,8 +4996,11 @@ public struct CanvasState: Codable, Sendable { public var status: String? /// Renderer-targeted address for the opaque canvas content — either a /// directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) - /// or a channel-served `ahp-canvas-content://` address the - /// renderer resolves over this channel with `canvasReadResource`. The + /// or a content-reference URI the renderer resolves with the general + /// `resourceRead` command. Resolving over `resourceRead` keeps content flowing + /// entirely over the AHP transport, so a relayed or brokered deployment — + /// where the host is reachable only over AHP and cannot be dialed directly — + /// can still serve every byte, with no port or direct connection required. The /// renderer dispatches on the scheme and enforces its URL policy. See /// {@link /specification/canvas-channel | Canvas Channel}. public var url: String? @@ -5023,8 +5012,7 @@ public struct CanvasState: Codable, Sendable { public init( instanceId: String, canvasId: String, - extensionId: String, - extensionName: String? = nil, + providerId: String, displayName: String? = nil, input: [String: AnyCodable]? = nil, title: String? = nil, @@ -5035,8 +5023,7 @@ public struct CanvasState: Codable, Sendable { ) { self.instanceId = instanceId self.canvasId = canvasId - self.extensionId = extensionId - self.extensionName = extensionName + self.providerId = providerId self.displayName = displayName self.input = input self.title = title diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 9c4e6968..08179bb4 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -1302,11 +1302,11 @@ public func resourceWatchReducer(state: ResourceWatchState, action: StateAction) /// /// `canvas/updated` is a sparse merge — a presented field (title, status, url, /// availability) overwrites the corresponding `CanvasState` field while an -/// absent field preserves the current value. `canvas/closeRequested` and -/// `canvas/message` are pure client→host signals the host acts on out of band, -/// mirroring how `terminal/input` is side-effect-only, so they leave the state -/// unchanged. Unknown action types degrade gracefully so a client speaking an -/// older protocol stays correct if the server adds new `canvas/*` actions in a +/// absent field preserves the current value. `canvas/closeRequested` is a +/// pure client→host signal the host acts on out of band, mirroring how +/// `terminal/input` is side-effect-only, so it leaves the state unchanged. +/// Unknown action types degrade gracefully so a client speaking an older +/// protocol stays correct if the server adds new `canvas/*` actions in a /// future version. public func canvasReducer(state: CanvasState, action: StateAction) -> CanvasState { switch action { @@ -1321,9 +1321,6 @@ public func canvasReducer(state: CanvasState, action: StateAction) -> CanvasStat case .canvasCloseRequested: return state - case .canvasMessage: - return state - default: return state } diff --git a/docs/.changes/20260709-canvas-channel.json b/docs/.changes/20260709-canvas-channel.json index d10462c3..dbd85306 100644 --- a/docs/.changes/20260709-canvas-channel.json +++ b/docs/.changes/20260709-canvas-channel.json @@ -1,5 +1,5 @@ { "type": "added", - "message": "Canvas channel — an opt-in surface for the agent to open rich, interactive UI surfaces (document/spreadsheet editors, diff views, live previews) alongside a session, following the \"one channel per resource\" model of terminals and changesets. Adds the `ClientCapabilities.canvas` capability; the `SessionState.canvases` registry and `SessionState.openCanvases` catalogue (`session/canvasesChanged` / `session/openCanvasesChanged`) plus `SessionActiveClient.canvasProviders` and the `SessionCanvasDeclaration` / `ClientCanvasDeclaration` / `OpenCanvasRef` / `CanvasProviderSource` discovery types; the per-instance `ahp-canvas:/` channel and its `CanvasState`; the `canvas/updated`, `canvas/closeRequested`, and `canvas/message` actions; the `canvasOpen` / `canvasInvokeAction` / `canvasClose` provider request family and the client → server `canvasReadResource` content-fetch request; and the `CanvasProviderError` (`-32012`) error.", + "message": "Canvas channel — an opt-in surface for the agent to open rich, interactive UI surfaces (document/spreadsheet editors, diff views, live previews) alongside a session, following the \"one channel per resource\" model of terminals and changesets. Adds the `ClientCapabilities.canvas` capability; the `SessionState.canvases` registry and `SessionState.openCanvases` catalogue (`session/canvasesChanged` / `session/openCanvasesChanged`) plus `SessionActiveClient.canvasProviders` and the `SessionCanvasDeclaration` / `ClientCanvasDeclaration` / `SessionCanvasOperation` / `OpenCanvasRef` / `CanvasProviderSource` discovery types; the per-instance `ahp-canvas:/` channel and its `CanvasState`; the `canvas/updated` and `canvas/closeRequested` actions; the `canvasOpen` / `canvasInvokeOperation` / `canvasClose` provider request family; and the `CanvasProviderError` (`-32012`) error. Canvas content is resolved over the general `resourceRead` command, so it works over a relay with no direct host connection.", "issues": [297] } diff --git a/docs/guide/canvases.md b/docs/guide/canvases.md index d4b009c6..8b51527e 100644 --- a/docs/guide/canvases.md +++ b/docs/guide/canvases.md @@ -27,25 +27,22 @@ Both lists are published with full-replacement actions (`session/canvasesChanged The defining idea: **a renderer renders from state, not from requests.** Subscribing to a canvas instance yields a `CanvasState` with a `url`, and the renderer resolves that address: - a directly-loadable `https:`, in-process, or `http://localhost` address is loaded straight into the surface; or -- an `ahp-canvas-content://` address is resolved over the instance's own channel with `canvasReadResource` — the escape hatch for relayed deployments where the renderer can't reach the host's network directly. +- a content-reference address is resolved over the general `resourceRead` command — which keeps content flowing over the AHP transport itself, the escape hatch for relayed deployments where the renderer can't reach the host's network directly. When the canvas changes, the provider emits a `canvas/updated` action that sparse-merges the new `title` / `status` / `url` / `availability` into state, and every subscriber re-renders. There is no imperative "draw this" message anywhere in the protocol. ## Interaction -Two paths carry interaction back to the provider: +Interaction travels back to the provider through **declared operations.** A canvas can declare named operations (with input schemas) the agent may invoke; the host issues a `canvasInvokeOperation` request to the provider and returns its opaque result. Opening and closing are the same shape (`canvasOpen`, `canvasClose`). For a host-provided canvas the host resolves these internally and sends no request. -- **Declared actions.** A canvas can declare named actions (with input schemas) the agent may invoke; the host issues a `canvasInvokeAction` request to the provider and returns its opaque result. Opening and closing are the same shape (`canvasOpen`, `canvasClose`). For a host-provided canvas the host resolves these internally and sends no request. -- **The message bridge.** `canvas/message` relays an opaque payload between the rendered View and the provider in either direction — the relay-carried analogue of a `postMessage` bridge. Like `terminal/input`, it is a pure signal with a no-op reducer, so it never accumulates in state. - -Closing follows the same signal-then-request pattern: the user hits ✕, the renderer dispatches `canvas/closeRequested`, and the host runs the close flow — resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. +Closing follows a signal-then-request pattern: the user hits ✕, the renderer dispatches `canvas/closeRequested`, and the host runs the close flow — resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. ## Lifecycle 1. A client declares `ClientCapabilities.canvas` and publishes any `canvasProviders`. The host folds them into `SessionState.canvases`. 2. The agent opens a canvas. The host mints an instance, sends `canvasOpen` to the provider (or resolves it internally), and adds an `OpenCanvasRef` to `SessionState.openCanvases`. 3. A renderer subscribes to the instance's `ahp-canvas:/` channel, receives the `CanvasState` snapshot, and loads its `url`. -4. The provider pushes `canvas/updated` as the canvas evolves; the agent invokes declared actions; the View and provider exchange `canvas/message` payloads. +4. The provider pushes `canvas/updated` as the canvas evolves, and the agent invokes declared operations. 5. The user (or the agent) closes the canvas. The host resolves `canvasClose` and republishes `openCanvases` without the instance; subscribers see the channel go away. See the [Canvas Channel specification](/specification/canvas-channel) for the exact state shapes, action semantics, command signatures, and error codes. diff --git a/docs/specification/canvas-channel.md b/docs/specification/canvas-channel.md index a79615ca..9e3c7354 100644 --- a/docs/specification/canvas-channel.md +++ b/docs/specification/canvas-channel.md @@ -2,7 +2,7 @@ A canvas is a rich, interactive UI surface the agent can open alongside a session — a document editor, a diff view, a live preview, a spreadsheet, a browser pane. Each open canvas is a first-class subscribable resource with its own `ahp-canvas:/` channel, mirroring the "one channel per resource" convention used by terminals, changesets, and resource watches. -Rendering is **state-driven**: a client renders a canvas by reading its [`CanvasState.url`](#state) and resolving that address per the renderer's URL policy. The host never sends a "render this" request — it publishes state, and the renderer reacts. Interaction flows back to whichever peer *provides* the canvas through a small server ↔ client request family (`canvasOpen` / `canvasInvokeAction` / `canvasClose`) and an opaque `canvas/message` bridge. +Rendering is **state-driven**: a client renders a canvas by reading its [`CanvasState.url`](#state) and resolving that address per the renderer's URL policy. The host never sends a "render this" request — it publishes state, and the renderer reacts. Interaction flows back to whichever peer *provides* the canvas through a small server ↔ client request family (`canvasOpen` / `canvasInvokeOperation` / `canvasClose`). ## Capability @@ -14,7 +14,7 @@ ClientCapabilities { } ``` -A client that declares `canvas` can both **render** an opaque canvas URL in an isolated surface and **provide** canvases — answering `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for the providers it publishes. Hosts SHOULD only populate [`SessionState.canvases`](#discovery) / [`SessionState.openCanvases`](#discovery) and only route canvas requests to a client that declared the capability. Clients that omit it see no canvas surface at all. +A client that declares `canvas` can both **render** an opaque canvas URL in an isolated surface and **provide** canvases — answering `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for the providers it publishes. Hosts SHOULD only populate [`SessionState.canvases`](#discovery) / [`SessionState.openCanvases`](#discovery) and only route canvas requests to a client that declared the capability. Clients that omit it see no canvas surface at all. ## Discovery @@ -38,40 +38,41 @@ SessionActiveClient { } ``` -The host folds each `ClientCanvasDeclaration` into `canvases`, filling in the owning `extensionId` and setting `source` to the client variant so it knows where to route later requests. +The host folds each `ClientCanvasDeclaration` into `canvases`, filling in the owning `providerId` and setting `source` to the client variant so it knows where to route later requests. ### Declarations ```typescript // One entry in the aggregated SessionState.canvases registry. SessionCanvasDeclaration { - extensionId: string // owning provider id - extensionName?: string - canvasId: string // provider-local id, unique within extensionId + providerId: string // owning provider id (opaque to AHP) + canvasId: string // provider-local id, unique within providerId displayName: string description: string inputSchema?: object // JSON Schema for the open input (opaque to AHP) - actions?: SessionCanvasAction[] + operations?: SessionCanvasOperation[] source: CanvasProviderSource // where it came from — for routing and cleanup } -// The lighter shape a client publishes; the host derives extensionId + source. +// The lighter shape a client publishes; the host derives providerId + source. ClientCanvasDeclaration { canvasId: string displayName: string description: string inputSchema?: object - actions?: SessionCanvasAction[] + operations?: SessionCanvasOperation[] } -// One named action a canvas exposes to the agent. -SessionCanvasAction { - name: string // unique within the owning (extensionId, canvasId) +// One named operation a canvas exposes to the agent. +SessionCanvasOperation { + name: string // unique within the owning (providerId, canvasId) description?: string inputSchema?: object } ``` +`providerId` is an opaque namespace string AHP does not interpret; it is carried so a provider-local `canvasId` stays unique when several providers each declare a canvas of the same id. + `CanvasProviderSource` is a discriminated union over `CanvasProviderKind` recording who owns the callbacks for a canvas: ```typescript @@ -81,15 +82,12 @@ CanvasClientProviderSource { kind: 'client', clientId } // a connected client ### Open-instance catalogue -Each open instance is summarised by an `OpenCanvasRef`, which carries the `ahp-canvas:/` URI to subscribe to: +Each open instance is summarised by an `OpenCanvasRef`. The instance is identified solely by its channel URI; the other identity fields (`instanceId`, `providerId`) live on the full [`CanvasState`](#state): ```typescript OpenCanvasRef { - instanceId: string channel: URI // ahp-canvas:/ — subscribe for full CanvasState - canvasId: string - extensionId: string - extensionName?: string + canvasId: string // for picking the right native renderer title?: string // mirrored from CanvasState.title availability: CanvasAvailability } @@ -111,8 +109,7 @@ Subscribing to an instance's channel yields the authoritative, mutable per-insta CanvasState { instanceId: string // server-assigned handle, unique within the session canvasId: string // provider-local id this instance was opened from - extensionId: string // owning provider id - extensionName?: string + providerId: string // owning provider id (opaque to AHP) displayName?: string input?: object // the open input, retained for resume/rebind title?: string @@ -130,25 +127,24 @@ CanvasAvailability = 'ready' | 'stale' // 'stale' = provider currently unavail A renderer displays a canvas by dispatching on the scheme of `CanvasState.url`: - A **directly-loadable** address — `https:`, an in-process scheme, or `http://localhost` — is loaded straight into the isolated surface, subject to the renderer's own URL policy. -- An `ahp-canvas-content://` address is **channel-served**: in a relayed deployment the renderer cannot dial the host directly, so it resolves the bytes over the instance's existing channel with [`canvasReadResource`](#commands) instead of loading a network URL. The `` segment names which canvas channel to read from. Sub-resources the loaded document references (stylesheets, images) are fetched the same way. +- A **content-reference** address is resolved over the general [`resourceRead`](/reference/common#resourceread) command rather than dialed directly. This keeps content flowing entirely over the AHP transport, so a relayed or brokered deployment — where the host is reachable only over AHP and cannot be dialed directly — can still serve every byte, with no port or direct connection required. Sub-resources the loaded document references (stylesheets, images) are fetched the same way. -The canvas content itself is opaque to AHP — the protocol carries only the address and, when channel-served, the resolved bytes. +The canvas content itself is opaque to AHP — the protocol carries only the address and, when resolved over `resourceRead`, the returned bytes. ## Actions -Three actions travel on the per-instance channel. Because the channel is scoped to a single instance by the action envelope, none of them carry an `instanceId`. +Two actions travel on the per-instance channel. Because the channel is scoped to a single instance by the action envelope, neither carries an `instanceId`. | Action | Client-dispatchable | Reducer effect | |---|:---:|---| | `canvas/updated` | Yes | Sparse-merges `title` / `status` / `url` / `availability` into `CanvasState`. | | `canvas/closeRequested` | Yes | No-op — signals the host to run the close flow. | -| `canvas/message` | Yes | No-op — opaque View ↔ provider message bridge. | `canvas/updated` is dispatched by the server for a server-side provider, or by the client that provides an instance to push its own presentation changes — the same client-dispatchable pattern as `terminal/titleChanged`, and the only way a client-side provider updates the structured `CanvasState` (and the `OpenCanvasRef` fields mirrored from it) that other subscribers render. The host stays the authoritative reducer: it SHOULD reject an update from a client that is not the instance's resolved provider, then applies the merge and re-broadcasts. It uses **sparse-merge** semantics: each present field overwrites the corresponding `CanvasState` field, and an absent field preserves the current value. There is no clear-to-absent through this action — a provider that needs to reset a field re-publishes it, and a full reset arrives as a fresh `CanvasState` snapshot on (re)subscribe. -`canvas/closeRequested` and `canvas/message` are pure signals with no-op reducers, mirroring how `terminal/input` is a side-effect-only client action — they never bloat channel state. `canvas/closeRequested` is a client → host signal (the user hit ✕); the host responds by resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. `canvas/message` is bidirectional: a client dispatches it to carry a View → provider message, and the host emits it to carry a provider → View message. A View → provider message is routed to the instance's single resolved provider — relayed to that client when the provider and renderer are different clients, or handled host-internally for a server-side provider. Targeting a particular renderer for a provider → View message when an instance has more than one renderer is not yet specified; it depends on the still-open render-targeting question and is out of scope here. +`canvas/closeRequested` is a pure signal with a no-op reducer, mirroring how `terminal/input` is a side-effect-only client action — it never bloats channel state. It is a client → host signal (the user hit ✕); the host responds by resolving `canvasClose` against the provider and dropping the instance from `openCanvases`. ## Provider request family @@ -166,15 +162,15 @@ sequenceDiagram H->>C: action session/canvasesChanged (registry) A->>H: open canvas "diff" - H->>C: canvasOpen { canvasId, extensionId, instanceId, input } + H->>C: canvasOpen { canvasId, providerId, instanceId, input } C-->>H: { url, title } H->>C: action session/openCanvasesChanged (instance added) C->>H: subscribe { channel: "ahp-canvas:/abc" } H-->>C: snapshot { state: CanvasState } Note over C: renderer loads CanvasState.url - A->>H: invoke action "format" - H->>C: canvasInvokeAction { instanceId, actionName, input } + A->>H: invoke operation "format" + H->>C: canvasInvokeOperation { instanceId, operationName, input } C-->>H: { value } C-->>H: action canvas/updated { status } @@ -188,13 +184,10 @@ sequenceDiagram | Method | Channel | Direction | Purpose | |---|---|---|---| | `canvasOpen` | `ahp-session:/` | Client ↔ Server | Open an instance against its provider. Returns the initial `url` / `title` / `status`. | -| `canvasInvokeAction` | `ahp-session:/` | Client ↔ Server | Invoke a declared action on an open instance. Returns an opaque provider value. | +| `canvasInvokeOperation` | `ahp-session:/` | Client ↔ Server | Invoke a declared operation on an open instance. Returns an opaque provider value. | | `canvasClose` | `ahp-session:/` | Client ↔ Server | Close an instance against its provider, as part of the close flow. | -| `canvasReadResource` | `ahp-canvas:/` | Client → Server | Resolve an `ahp-canvas-content:` address over the instance's channel. Returns `contents: CanvasResourceContent[]`. | - -`canvasReadResource` is modeled on MCP's `resources/read`; each `CanvasResourceContent` carries the resolved `uri`, an optional `mimeType`, and exactly one of `text` (UTF-8) or `blob` (base64). -The channel split within the family is deliberate: the three provider operations are session-scoped RPCs and travel on the session channel (`ahp-session:/`), while `canvasReadResource` resolves content bytes and so targets the instance's own channel (`ahp-canvas:/`). +The three provider operations are session-scoped RPCs and travel on the session channel (`ahp-session:/`). Content the renderer needs is resolved separately over the general [`resourceRead`](/reference/common#resourceread) command, so the canvas family carries no content-fetch method of its own. ### Actions @@ -202,11 +195,9 @@ The channel split within the family is deliberate: the three provider operations |---|---| | `canvas/updated` | client → host (a client-side provider) **or** host → client | | `canvas/closeRequested` | client → host | -| `canvas/message` | client → host **or** host → client | ## Errors | Code | Name | When | |---|---|---| -| `-32012` | `CanvasProviderError` | A `canvasOpen` / `canvasInvokeAction` / `canvasClose` request failed. The `data` field MUST be a `CanvasProviderErrorData` carrying the provider-defined `{ code, message }` — for example `canvas_action_no_handler` when the provider declared the canvas but has no handler for the action, or `canvas_provider_unavailable` when the provider is disconnected. | -| `-32008` | `NotFound` | A `canvasReadResource` content URI does not resolve. | +| `-32012` | `CanvasProviderError` | A `canvasOpen` / `canvasInvokeOperation` / `canvasClose` request failed. The `data` field MUST be a `CanvasProviderErrorData` carrying the provider-defined `{ code, message }` — for example `canvas_action_no_handler` when the provider declared the canvas but has no handler for the operation, or `canvas_provider_unavailable` when the provider is disconnected. | diff --git a/docs/specification/subscriptions.md b/docs/specification/subscriptions.md index 8560d669..0ddbcf27 100644 --- a/docs/specification/subscriptions.md +++ b/docs/specification/subscriptions.md @@ -8,10 +8,10 @@ The channel concept is woven into every wire message. **Every command and every | Direction | Methods | `channel` value | |---|---|---| -| Client → Server commands (channel-scoped) | `subscribe`, `createSession`, `disposeSession`, `createTerminal`, `disposeTerminal`, `fetchTurns`, `completions`, `invokeChangesetOperation`, `canvasReadResource` | The target channel's URI (e.g. `ahp-session:/`, or `ahp-canvas:/` for `canvasReadResource`). | +| Client → Server commands (channel-scoped) | `subscribe`, `createSession`, `disposeSession`, `createTerminal`, `disposeTerminal`, `fetchTurns`, `completions`, `invokeChangesetOperation` | The target channel's URI (e.g. `ahp-session:/`). | | Client → Server commands (connection-level) | `initialize`, `ping`, `reconnect`, `listSessions`, `authenticate`, `resolveSessionConfig`, `sessionConfigCompletions`, `resourceRead`, `resourceWrite`, `resourceList`, `resourceCopy`, `resourceDelete`, `resourceMove`, `resourceResolve`, `resourceMkdir`, `resourceRequest`, `createResourceWatch` | Literal `'ahp-root://'`. | | Server → Client commands (bidirectional `resource*` family) | The same nine `resource*` request methods plus `createResourceWatch` may also be initiated by the server. Used for host-driven per-session filesystem providers and for fetching client-published URIs (e.g. `virtual://my-client/...` plugins). | Literal `'ahp-root://'`. | -| Server → Client commands (canvas provider family) | `canvasOpen`, `canvasInvokeAction`, `canvasClose` — initiated by the host against the client that declared the target canvas provider; mirrored in the client → server map for symmetry. See [Canvas Channel](/specification/canvas-channel). | The owning `ahp-session:/` URI. | +| Server → Client commands (canvas provider family) | `canvasOpen`, `canvasInvokeOperation`, `canvasClose` — initiated by the host against the client that declared the target canvas provider; mirrored in the client → server map for symmetry. See [Canvas Channel](/specification/canvas-channel). | The owning `ahp-session:/` URI. | | Client → Server `dispatchAction` | The channel the action targets. | | Client → Server `unsubscribe` | The channel being unsubscribed. | | Server → Client `action` | The channel that owns the action envelope. | diff --git a/schema/actions.schema.json b/schema/actions.schema.json index ffc4a32c..45a4d44f 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2005,22 +2005,6 @@ "type" ] }, - "CanvasMessageAction": { - "type": "object", - "description": "An opaque message relayed between the rendered canvas View and the\ninstance's provider — the relay-carried analogue of a `postMessage` bridge.\n\nBidirectional: a client dispatches it to carry a View→provider message, and\nthe host emits it to carry a provider→View message (routed to the provider\nresolved for the instance, or handled host-internally for a server-side\nprovider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it\nis a pure signal with a no-op reducer, so it never bloats channel state. See\n{@link /specification/canvas-channel | Canvas Channel}.", - "properties": { - "type": { - "const": "canvas/message" - }, - "payload": { - "description": "Opaque, provider-defined message payload." - } - }, - "required": [ - "type", - "payload" - ] - }, "ChatToolCallConfirmedAction": { "oneOf": [ { @@ -2939,7 +2923,7 @@ "items": { "$ref": "#/$defs/ClientCanvasDeclaration" }, - "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them\nback to this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -2961,7 +2945,7 @@ }, "CanvasClientProviderSource": { "type": "object", - "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this\ncanvas to the identified client.", "properties": { "kind": { "const": "client" @@ -2976,22 +2960,22 @@ "clientId" ] }, - "SessionCanvasAction": { + "SessionCanvasOperation": { "type": "object", - "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "description": "One named operation a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(providerId, canvasId)`. Named \"operation\" to match the changeset channel's\noperation vocabulary.", "properties": { "name": { "type": "string", - "description": "Action name, unique within the owning `(extensionId, canvasId)`." + "description": "Operation name, unique within the owning `(providerId, canvasId)`." }, "description": { "type": "string", - "description": "Human-readable description of what the action does." + "description": "Human-readable description of what the operation does." }, "inputSchema": { "type": "object", "additionalProperties": {}, - "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + "description": "JSON Schema for the operation's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." } }, "required": [ @@ -3002,17 +2986,13 @@ "type": "object", "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", "properties": { - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id. Stable across declarations and instances." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers.\nStable across declarations and instances." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id. Unique within `extensionId`." + "description": "Provider-local canvas id. Unique within `providerId`." }, "displayName": { "type": "string", @@ -3027,12 +3007,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." }, "source": { "$ref": "#/$defs/CanvasProviderSource", @@ -3040,7 +3020,7 @@ } }, "required": [ - "extensionId", + "providerId", "canvasId", "displayName", "description", @@ -3049,7 +3029,7 @@ }, "ClientCanvasDeclaration": { "type": "object", - "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`providerId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", "properties": { "canvasId": { "type": "string", @@ -3068,12 +3048,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." } }, "required": [ @@ -3084,27 +3064,15 @@ }, "OpenCanvasRef": { "type": "object", - "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.\n\nThe instance is identified solely by its {@link channel} URI — the other\nidentity fields (`instanceId`, `providerId`) live on the full\n{@link CanvasState}, not here.", "properties": { - "instanceId": { - "type": "string", - "description": "Server-assigned instance handle, unique within the session." - }, "channel": { "$ref": "#/$defs/URI", - "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + "description": "The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the\nopen canvas; subscribe to it to load the full {@link CanvasState}." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id this instance was opened from." - }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Provider-local canvas id this instance was opened from. Retained so a\ncatalogue view can pick the right native renderer without subscribing." }, "title": { "type": "string", @@ -3116,10 +3084,8 @@ } }, "required": [ - "instanceId", "channel", "canvasId", - "extensionId", "availability" ] }, @@ -6848,7 +6814,7 @@ }, "CanvasState": { "type": "object", - "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over the general `resourceRead` command for a\ncontent-reference address. It never receives a \"render this\" request.", "properties": { "instanceId": { "type": "string", @@ -6858,13 +6824,9 @@ "type": "string", "description": "Provider-local canvas id this instance was opened from." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers." }, "displayName": { "type": "string", @@ -6885,7 +6847,7 @@ }, "url": { "type": "string", - "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a content-reference URI the renderer resolves with the general\n`resourceRead` command. Resolving over `resourceRead` keeps content flowing\nentirely over the AHP transport, so a relayed or brokered deployment —\nwhere the host is reachable only over AHP and cannot be dialed directly —\ncan still serve every byte, with no port or direct connection required. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." }, "availability": { "$ref": "#/$defs/CanvasAvailability", @@ -6899,7 +6861,7 @@ "required": [ "instanceId", "canvasId", - "extensionId", + "providerId", "availability", "provider" ] @@ -7551,9 +7513,6 @@ }, { "$ref": "#/$defs/CanvasCloseRequestedAction" - }, - { - "$ref": "#/$defs/CanvasMessageAction" } ] }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index e1145653..9376cdd6 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -122,7 +122,7 @@ "canvas": { "type": "object", "additionalProperties": {}, - "description": "Client can render canvases and host client-declared canvas providers — it\ncan render an opaque canvas URL in an isolated surface, and it can answer\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases\nit declares via {@link SessionActiveClient.canvasProviders}.\n\nHosts SHOULD only populate {@link SessionState.canvases} /\n{@link SessionState.openCanvases} and only route canvas requests to a\nclient that declared this capability. Clients that omit it see no canvas\nsurface. See {@link /specification/canvas-channel | Canvas Channel}." + "description": "Client can render canvases and host client-declared canvas providers — it\ncan render an opaque canvas URL in an isolated surface, and it can answer\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases\nit declares via {@link SessionActiveClient.canvasProviders}.\n\nHosts SHOULD only populate {@link SessionState.canvases} /\n{@link SessionState.openCanvases} and only route canvas requests to a\nclient that declared this capability. Clients that omit it see no canvas\nsurface. See {@link /specification/canvas-channel | Canvas Channel}." } } }, @@ -1320,9 +1320,9 @@ "type": "string", "description": "Provider-local canvas id to open." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." + "description": "Owning provider id (opaque to AHP)." }, "instanceId": { "type": "string", @@ -1337,7 +1337,7 @@ "required": [ "channel", "canvasId", - "extensionId", + "providerId", "instanceId" ] }, @@ -1359,9 +1359,9 @@ } } }, - "CanvasInvokeActionParams": { + "CanvasInvokeOperationParams": { "type": "object", - "description": "Invokes one of a canvas's declared actions against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) when the agent invokes a\n{@link SessionCanvasAction | declared action} on an open instance. The\nprovider returns an opaque, provider-defined value. Registered symmetrically\nwith the rest of the provider family (see {@link CanvasOpenParams}).", + "description": "Invokes one of a canvas's declared operations against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) when the agent invokes a\n{@link SessionCanvasOperation | declared operation} on an open instance. The\nprovider returns an opaque, provider-defined value. Registered symmetrically\nwith the rest of the provider family (see {@link CanvasOpenParams}).", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1369,37 +1369,37 @@ }, "instanceId": { "type": "string", - "description": "Instance handle the action targets." + "description": "Instance handle the operation targets." }, "canvasId": { "type": "string", "description": "Provider-local canvas id of the instance." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." + "description": "Owning provider id (opaque to AHP)." }, - "actionName": { + "operationName": { "type": "string", - "description": "Declared action name to invoke." + "description": "Declared operation name to invoke." }, "input": { "type": "object", "additionalProperties": {}, - "description": "Action input, validated by the provider against its declared schema." + "description": "Operation input, validated by the provider against its declared schema." } }, "required": [ "channel", "instanceId", "canvasId", - "extensionId", - "actionName" + "providerId", + "operationName" ] }, - "CanvasInvokeActionResult": { + "CanvasInvokeOperationResult": { "type": "object", - "description": "Result of the `canvasInvokeAction` command.", + "description": "Result of the `canvasInvokeOperation` command.", "properties": { "value": { "description": "Opaque, provider-defined return value." @@ -1422,75 +1422,16 @@ "type": "string", "description": "Provider-local canvas id of the instance." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." + "description": "Owning provider id (opaque to AHP)." } }, "required": [ "channel", "instanceId", "canvasId", - "extensionId" - ] - }, - "CanvasReadResourceParams": { - "type": "object", - "description": "Reads channel-served canvas content by `ahp-canvas-content:` URI.\n\nA client → host request, modeled on MCP's `resources/read`. When a canvas's\n{@link CanvasState.url} (or a sub-resource the rendered document references)\nis an `ahp-canvas-content://` address, the renderer cannot\ndial the host directly — for example in a relayed deployment behind a broker\n— so it resolves the bytes over the instance's existing `ahp-canvas:/`\nchannel with this request instead of loading a network URL. The ``\nsegment of the URI identifies which canvas channel to read from. See\n{@link /specification/canvas-channel | Canvas Channel}.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "The owning canvas channel URI (`ahp-canvas:/`)." - }, - "uri": { - "type": "string", - "description": "An `ahp-canvas-content://` content URI to read." - } - }, - "required": [ - "channel", - "uri" - ] - }, - "CanvasReadResourceResult": { - "type": "object", - "description": "Result of the `canvasReadResource` command.", - "properties": { - "contents": { - "type": "array", - "items": { - "$ref": "#/$defs/CanvasResourceContent" - }, - "description": "The resolved content parts, wrapped for forward compatibility." - } - }, - "required": [ - "contents" - ] - }, - "CanvasResourceContent": { - "type": "object", - "description": "One resolved piece of channel-served canvas content.\n\nCarries exactly one of {@link text} (text payloads) or {@link blob}\n(base64-encoded binary payloads).", - "properties": { - "uri": { - "type": "string", - "description": "The content URI this part resolves." - }, - "mimeType": { - "type": "string", - "description": "MIME type of the content, when known." - }, - "text": { - "type": "string", - "description": "UTF-8 text content, for text payloads." - }, - "blob": { - "type": "string", - "description": "Base64-encoded content, for binary payloads." - } - }, - "required": [ - "uri" + "providerId" ] }, "Icon": { @@ -2321,7 +2262,7 @@ "items": { "$ref": "#/$defs/ClientCanvasDeclaration" }, - "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them\nback to this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -2343,7 +2284,7 @@ }, "CanvasClientProviderSource": { "type": "object", - "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this\ncanvas to the identified client.", "properties": { "kind": { "const": "client" @@ -2358,22 +2299,22 @@ "clientId" ] }, - "SessionCanvasAction": { + "SessionCanvasOperation": { "type": "object", - "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "description": "One named operation a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(providerId, canvasId)`. Named \"operation\" to match the changeset channel's\noperation vocabulary.", "properties": { "name": { "type": "string", - "description": "Action name, unique within the owning `(extensionId, canvasId)`." + "description": "Operation name, unique within the owning `(providerId, canvasId)`." }, "description": { "type": "string", - "description": "Human-readable description of what the action does." + "description": "Human-readable description of what the operation does." }, "inputSchema": { "type": "object", "additionalProperties": {}, - "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + "description": "JSON Schema for the operation's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." } }, "required": [ @@ -2384,17 +2325,13 @@ "type": "object", "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", "properties": { - "extensionId": { - "type": "string", - "description": "Owning provider id. Stable across declarations and instances." - }, - "extensionName": { + "providerId": { "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers.\nStable across declarations and instances." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id. Unique within `extensionId`." + "description": "Provider-local canvas id. Unique within `providerId`." }, "displayName": { "type": "string", @@ -2409,12 +2346,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." }, "source": { "$ref": "#/$defs/CanvasProviderSource", @@ -2422,7 +2359,7 @@ } }, "required": [ - "extensionId", + "providerId", "canvasId", "displayName", "description", @@ -2431,7 +2368,7 @@ }, "ClientCanvasDeclaration": { "type": "object", - "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`providerId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", "properties": { "canvasId": { "type": "string", @@ -2450,12 +2387,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." } }, "required": [ @@ -2466,27 +2403,15 @@ }, "OpenCanvasRef": { "type": "object", - "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.\n\nThe instance is identified solely by its {@link channel} URI — the other\nidentity fields (`instanceId`, `providerId`) live on the full\n{@link CanvasState}, not here.", "properties": { - "instanceId": { - "type": "string", - "description": "Server-assigned instance handle, unique within the session." - }, "channel": { "$ref": "#/$defs/URI", - "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + "description": "The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the\nopen canvas; subscribe to it to load the full {@link CanvasState}." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id this instance was opened from." - }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Provider-local canvas id this instance was opened from. Retained so a\ncatalogue view can pick the right native renderer without subscribing." }, "title": { "type": "string", @@ -2498,10 +2423,8 @@ } }, "required": [ - "instanceId", "channel", "canvasId", - "extensionId", "availability" ] }, @@ -6230,7 +6153,7 @@ }, "CanvasState": { "type": "object", - "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over the general `resourceRead` command for a\ncontent-reference address. It never receives a \"render this\" request.", "properties": { "instanceId": { "type": "string", @@ -6240,13 +6163,9 @@ "type": "string", "description": "Provider-local canvas id this instance was opened from." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers." }, "displayName": { "type": "string", @@ -6267,7 +6186,7 @@ }, "url": { "type": "string", - "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a content-reference URI the renderer resolves with the general\n`resourceRead` command. Resolving over `resourceRead` keeps content flowing\nentirely over the AHP transport, so a relayed or brokered deployment —\nwhere the host is reachable only over AHP and cannot be dialed directly —\ncan still serve every byte, with no port or direct connection required. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." }, "availability": { "$ref": "#/$defs/CanvasAvailability", @@ -6281,7 +6200,7 @@ "required": [ "instanceId", "canvasId", - "extensionId", + "providerId", "availability", "provider" ] @@ -8286,22 +8205,6 @@ "type" ] }, - "CanvasMessageAction": { - "type": "object", - "description": "An opaque message relayed between the rendered canvas View and the\ninstance's provider — the relay-carried analogue of a `postMessage` bridge.\n\nBidirectional: a client dispatches it to carry a View→provider message, and\nthe host emits it to carry a provider→View message (routed to the provider\nresolved for the instance, or handled host-internally for a server-side\nprovider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it\nis a pure signal with a no-op reducer, so it never bloats channel state. See\n{@link /specification/canvas-channel | Canvas Channel}.", - "properties": { - "type": { - "const": "canvas/message" - }, - "payload": { - "description": "Opaque, provider-defined message payload." - } - }, - "required": [ - "type", - "payload" - ] - }, "URI": { "type": "string", "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." @@ -8556,9 +8459,6 @@ }, { "$ref": "#/$defs/CanvasCloseRequestedAction" - }, - { - "$ref": "#/$defs/CanvasMessageAction" } ], "description": "Discriminated union of all state actions." diff --git a/schema/errors.schema.json b/schema/errors.schema.json index b19ddd83..e9580128 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -944,7 +944,7 @@ "items": { "$ref": "#/$defs/ClientCanvasDeclaration" }, - "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them\nback to this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -966,7 +966,7 @@ }, "CanvasClientProviderSource": { "type": "object", - "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this\ncanvas to the identified client.", "properties": { "kind": { "const": "client" @@ -981,22 +981,22 @@ "clientId" ] }, - "SessionCanvasAction": { + "SessionCanvasOperation": { "type": "object", - "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "description": "One named operation a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(providerId, canvasId)`. Named \"operation\" to match the changeset channel's\noperation vocabulary.", "properties": { "name": { "type": "string", - "description": "Action name, unique within the owning `(extensionId, canvasId)`." + "description": "Operation name, unique within the owning `(providerId, canvasId)`." }, "description": { "type": "string", - "description": "Human-readable description of what the action does." + "description": "Human-readable description of what the operation does." }, "inputSchema": { "type": "object", "additionalProperties": {}, - "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + "description": "JSON Schema for the operation's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." } }, "required": [ @@ -1007,17 +1007,13 @@ "type": "object", "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", "properties": { - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id. Stable across declarations and instances." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers.\nStable across declarations and instances." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id. Unique within `extensionId`." + "description": "Provider-local canvas id. Unique within `providerId`." }, "displayName": { "type": "string", @@ -1032,12 +1028,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." }, "source": { "$ref": "#/$defs/CanvasProviderSource", @@ -1045,7 +1041,7 @@ } }, "required": [ - "extensionId", + "providerId", "canvasId", "displayName", "description", @@ -1054,7 +1050,7 @@ }, "ClientCanvasDeclaration": { "type": "object", - "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`providerId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", "properties": { "canvasId": { "type": "string", @@ -1073,12 +1069,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." } }, "required": [ @@ -1089,27 +1085,15 @@ }, "OpenCanvasRef": { "type": "object", - "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.\n\nThe instance is identified solely by its {@link channel} URI — the other\nidentity fields (`instanceId`, `providerId`) live on the full\n{@link CanvasState}, not here.", "properties": { - "instanceId": { - "type": "string", - "description": "Server-assigned instance handle, unique within the session." - }, "channel": { "$ref": "#/$defs/URI", - "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + "description": "The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the\nopen canvas; subscribe to it to load the full {@link CanvasState}." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id this instance was opened from." - }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Provider-local canvas id this instance was opened from. Retained so a\ncatalogue view can pick the right native renderer without subscribing." }, "title": { "type": "string", @@ -1121,10 +1105,8 @@ } }, "required": [ - "instanceId", "channel", "canvasId", - "extensionId", "availability" ] }, @@ -4853,7 +4835,7 @@ }, "CanvasState": { "type": "object", - "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over the general `resourceRead` command for a\ncontent-reference address. It never receives a \"render this\" request.", "properties": { "instanceId": { "type": "string", @@ -4863,13 +4845,9 @@ "type": "string", "description": "Provider-local canvas id this instance was opened from." }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { + "providerId": { "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers." }, "displayName": { "type": "string", @@ -4890,7 +4868,7 @@ }, "url": { "type": "string", - "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a content-reference URI the renderer resolves with the general\n`resourceRead` command. Resolving over `resourceRead` keeps content flowing\nentirely over the AHP transport, so a relayed or brokered deployment —\nwhere the host is reachable only over AHP and cannot be dialed directly —\ncan still serve every byte, with no port or direct connection required. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." }, "availability": { "$ref": "#/$defs/CanvasAvailability", @@ -4904,7 +4882,7 @@ "required": [ "instanceId", "canvasId", - "extensionId", + "providerId", "availability", "provider" ] @@ -5026,7 +5004,7 @@ "canvas": { "type": "object", "additionalProperties": {}, - "description": "Client can render canvases and host client-declared canvas providers — it\ncan render an opaque canvas URL in an isolated surface, and it can answer\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases\nit declares via {@link SessionActiveClient.canvasProviders}.\n\nHosts SHOULD only populate {@link SessionState.canvases} /\n{@link SessionState.openCanvases} and only route canvas requests to a\nclient that declared this capability. Clients that omit it see no canvas\nsurface. See {@link /specification/canvas-channel | Canvas Channel}." + "description": "Client can render canvases and host client-declared canvas providers — it\ncan render an opaque canvas URL in an isolated surface, and it can answer\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases\nit declares via {@link SessionActiveClient.canvasProviders}.\n\nHosts SHOULD only populate {@link SessionState.canvases} /\n{@link SessionState.openCanvases} and only route canvas requests to a\nclient that declared this capability. Clients that omit it see no canvas\nsurface. See {@link /specification/canvas-channel | Canvas Channel}." } } }, @@ -6224,9 +6202,9 @@ "type": "string", "description": "Provider-local canvas id to open." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." + "description": "Owning provider id (opaque to AHP)." }, "instanceId": { "type": "string", @@ -6241,7 +6219,7 @@ "required": [ "channel", "canvasId", - "extensionId", + "providerId", "instanceId" ] }, @@ -6263,9 +6241,9 @@ } } }, - "CanvasInvokeActionParams": { + "CanvasInvokeOperationParams": { "type": "object", - "description": "Invokes one of a canvas's declared actions against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) when the agent invokes a\n{@link SessionCanvasAction | declared action} on an open instance. The\nprovider returns an opaque, provider-defined value. Registered symmetrically\nwith the rest of the provider family (see {@link CanvasOpenParams}).", + "description": "Invokes one of a canvas's declared operations against its provider.\n\nSent by the host to the providing client (or resolved host-internally for a\nserver-side provider) when the agent invokes a\n{@link SessionCanvasOperation | declared operation} on an open instance. The\nprovider returns an opaque, provider-defined value. Registered symmetrically\nwith the rest of the provider family (see {@link CanvasOpenParams}).", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -6273,37 +6251,37 @@ }, "instanceId": { "type": "string", - "description": "Instance handle the action targets." + "description": "Instance handle the operation targets." }, "canvasId": { "type": "string", "description": "Provider-local canvas id of the instance." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." + "description": "Owning provider id (opaque to AHP)." }, - "actionName": { + "operationName": { "type": "string", - "description": "Declared action name to invoke." + "description": "Declared operation name to invoke." }, "input": { "type": "object", "additionalProperties": {}, - "description": "Action input, validated by the provider against its declared schema." + "description": "Operation input, validated by the provider against its declared schema." } }, "required": [ "channel", "instanceId", "canvasId", - "extensionId", - "actionName" + "providerId", + "operationName" ] }, - "CanvasInvokeActionResult": { + "CanvasInvokeOperationResult": { "type": "object", - "description": "Result of the `canvasInvokeAction` command.", + "description": "Result of the `canvasInvokeOperation` command.", "properties": { "value": { "description": "Opaque, provider-defined return value." @@ -6326,75 +6304,16 @@ "type": "string", "description": "Provider-local canvas id of the instance." }, - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id." + "description": "Owning provider id (opaque to AHP)." } }, "required": [ "channel", "instanceId", "canvasId", - "extensionId" - ] - }, - "CanvasReadResourceParams": { - "type": "object", - "description": "Reads channel-served canvas content by `ahp-canvas-content:` URI.\n\nA client → host request, modeled on MCP's `resources/read`. When a canvas's\n{@link CanvasState.url} (or a sub-resource the rendered document references)\nis an `ahp-canvas-content://` address, the renderer cannot\ndial the host directly — for example in a relayed deployment behind a broker\n— so it resolves the bytes over the instance's existing `ahp-canvas:/`\nchannel with this request instead of loading a network URL. The ``\nsegment of the URI identifies which canvas channel to read from. See\n{@link /specification/canvas-channel | Canvas Channel}.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "The owning canvas channel URI (`ahp-canvas:/`)." - }, - "uri": { - "type": "string", - "description": "An `ahp-canvas-content://` content URI to read." - } - }, - "required": [ - "channel", - "uri" - ] - }, - "CanvasReadResourceResult": { - "type": "object", - "description": "Result of the `canvasReadResource` command.", - "properties": { - "contents": { - "type": "array", - "items": { - "$ref": "#/$defs/CanvasResourceContent" - }, - "description": "The resolved content parts, wrapped for forward compatibility." - } - }, - "required": [ - "contents" - ] - }, - "CanvasResourceContent": { - "type": "object", - "description": "One resolved piece of channel-served canvas content.\n\nCarries exactly one of {@link text} (text payloads) or {@link blob}\n(base64-encoded binary payloads).", - "properties": { - "uri": { - "type": "string", - "description": "The content URI this part resolves." - }, - "mimeType": { - "type": "string", - "description": "MIME type of the content, when known." - }, - "text": { - "type": "string", - "description": "UTF-8 text content, for text payloads." - }, - "blob": { - "type": "string", - "description": "Base64-encoded content, for binary payloads." - } - }, - "required": [ - "uri" + "providerId" ] }, "URI": { @@ -7219,9 +7138,6 @@ }, { "$ref": "#/$defs/CanvasCloseRequestedAction" - }, - { - "$ref": "#/$defs/CanvasMessageAction" } ], "description": "Discriminated union of all state actions." @@ -9165,22 +9081,6 @@ "type" ] }, - "CanvasMessageAction": { - "type": "object", - "description": "An opaque message relayed between the rendered canvas View and the\ninstance's provider — the relay-carried analogue of a `postMessage` bridge.\n\nBidirectional: a client dispatches it to carry a View→provider message, and\nthe host emits it to carry a provider→View message (routed to the provider\nresolved for the instance, or handled host-internally for a server-side\nprovider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it\nis a pure signal with a no-op reducer, so it never bloats channel state. See\n{@link /specification/canvas-channel | Canvas Channel}.", - "properties": { - "type": { - "const": "canvas/message" - }, - "payload": { - "description": "Opaque, provider-defined message payload." - } - }, - "required": [ - "type", - "payload" - ] - }, "ChatToolCallApprovedAction": { "type": "object", "description": "Client approves a pending tool call. The tool transitions to `running`.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index e9b7b2ba..89e305b7 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1082,7 +1082,7 @@ "items": { "$ref": "#/$defs/ClientCanvasDeclaration" }, - "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them\nback to this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -1104,7 +1104,7 @@ }, "CanvasClientProviderSource": { "type": "object", - "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this\ncanvas to the identified client.", "properties": { "kind": { "const": "client" @@ -1119,22 +1119,22 @@ "clientId" ] }, - "SessionCanvasAction": { + "SessionCanvasOperation": { "type": "object", - "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "description": "One named operation a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(providerId, canvasId)`. Named \"operation\" to match the changeset channel's\noperation vocabulary.", "properties": { "name": { "type": "string", - "description": "Action name, unique within the owning `(extensionId, canvasId)`." + "description": "Operation name, unique within the owning `(providerId, canvasId)`." }, "description": { "type": "string", - "description": "Human-readable description of what the action does." + "description": "Human-readable description of what the operation does." }, "inputSchema": { "type": "object", "additionalProperties": {}, - "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + "description": "JSON Schema for the operation's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." } }, "required": [ @@ -1145,17 +1145,13 @@ "type": "object", "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", "properties": { - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id. Stable across declarations and instances." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers.\nStable across declarations and instances." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id. Unique within `extensionId`." + "description": "Provider-local canvas id. Unique within `providerId`." }, "displayName": { "type": "string", @@ -1170,12 +1166,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." }, "source": { "$ref": "#/$defs/CanvasProviderSource", @@ -1183,7 +1179,7 @@ } }, "required": [ - "extensionId", + "providerId", "canvasId", "displayName", "description", @@ -1192,7 +1188,7 @@ }, "ClientCanvasDeclaration": { "type": "object", - "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`providerId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", "properties": { "canvasId": { "type": "string", @@ -1211,12 +1207,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." } }, "required": [ @@ -1227,27 +1223,15 @@ }, "OpenCanvasRef": { "type": "object", - "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.\n\nThe instance is identified solely by its {@link channel} URI — the other\nidentity fields (`instanceId`, `providerId`) live on the full\n{@link CanvasState}, not here.", "properties": { - "instanceId": { - "type": "string", - "description": "Server-assigned instance handle, unique within the session." - }, "channel": { "$ref": "#/$defs/URI", - "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + "description": "The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the\nopen canvas; subscribe to it to load the full {@link CanvasState}." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id this instance was opened from." - }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Provider-local canvas id this instance was opened from. Retained so a\ncatalogue view can pick the right native renderer without subscribing." }, "title": { "type": "string", @@ -1259,10 +1243,8 @@ } }, "required": [ - "instanceId", "channel", "canvasId", - "extensionId", "availability" ] }, @@ -4991,7 +4973,7 @@ }, "CanvasState": { "type": "object", - "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over the general `resourceRead` command for a\ncontent-reference address. It never receives a \"render this\" request.", "properties": { "instanceId": { "type": "string", @@ -5001,13 +4983,9 @@ "type": "string", "description": "Provider-local canvas id this instance was opened from." }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { + "providerId": { "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers." }, "displayName": { "type": "string", @@ -5028,7 +5006,7 @@ }, "url": { "type": "string", - "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a content-reference URI the renderer resolves with the general\n`resourceRead` command. Resolving over `resourceRead` keeps content flowing\nentirely over the AHP transport, so a relayed or brokered deployment —\nwhere the host is reachable only over AHP and cannot be dialed directly —\ncan still serve every byte, with no port or direct connection required. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." }, "availability": { "$ref": "#/$defs/CanvasAvailability", @@ -5042,7 +5020,7 @@ "required": [ "instanceId", "canvasId", - "extensionId", + "providerId", "availability", "provider" ] diff --git a/schema/state.schema.json b/schema/state.schema.json index 3dac5402..b29c1b5a 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -833,7 +833,7 @@ "items": { "$ref": "#/$defs/ClientCanvasDeclaration" }, - "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back\nto this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." + "description": "Canvas declarations this client contributes as a provider. Published\natomically with the rest of the active-client entry via\n`session/activeClientSet` — exactly like {@link tools} — so there is no\nseparate canvas-provider change action. The host folds these into\n{@link SessionState.canvases} with\n{@link SessionCanvasDeclaration.source | `source`} set to\n`{ kind: 'client', clientId }` and routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them\nback to this client. Only meaningful for a client that declared\n{@link ClientCapabilities.canvas}." } }, "required": [ @@ -855,7 +855,7 @@ }, "CanvasClientProviderSource": { "type": "object", - "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas\nto the identified client.", + "description": "A canvas provided by a connected client. The host routes\n`canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this\ncanvas to the identified client.", "properties": { "kind": { "const": "client" @@ -870,22 +870,22 @@ "clientId" ] }, - "SessionCanvasAction": { + "SessionCanvasOperation": { "type": "object", - "description": "One named action a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(extensionId, canvasId)`.", + "description": "One named operation a canvas exposes to the agent, mirroring the shape of a\n{@link ToolDefinition} entry. Unique within its owning\n`(providerId, canvasId)`. Named \"operation\" to match the changeset channel's\noperation vocabulary.", "properties": { "name": { "type": "string", - "description": "Action name, unique within the owning `(extensionId, canvasId)`." + "description": "Operation name, unique within the owning `(providerId, canvasId)`." }, "description": { "type": "string", - "description": "Human-readable description of what the action does." + "description": "Human-readable description of what the operation does." }, "inputSchema": { "type": "object", "additionalProperties": {}, - "description": "JSON Schema for the action's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." + "description": "JSON Schema for the operation's input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." } }, "required": [ @@ -896,17 +896,13 @@ "type": "object", "description": "One entry in the aggregated {@link SessionState.canvases} registry — a canvas\nthe agent can open, contributed by a server-side or client-declared provider.", "properties": { - "extensionId": { + "providerId": { "type": "string", - "description": "Owning provider id. Stable across declarations and instances." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers.\nStable across declarations and instances." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id. Unique within `extensionId`." + "description": "Provider-local canvas id. Unique within `providerId`." }, "displayName": { "type": "string", @@ -921,12 +917,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP; mirrors the\n{@link ToolDefinition.inputSchema} shape." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." }, "source": { "$ref": "#/$defs/CanvasProviderSource", @@ -934,7 +930,7 @@ } }, "required": [ - "extensionId", + "providerId", "canvasId", "displayName", "description", @@ -943,7 +939,7 @@ }, "ClientCanvasDeclaration": { "type": "object", - "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`extensionId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", + "description": "The lighter declaration shape a client publishes on\n{@link SessionActiveClient.canvasProviders}. The host derives the\n`providerId` and {@link SessionCanvasDeclaration.source | `source`} when\nfolding it into {@link SessionState.canvases}.", "properties": { "canvasId": { "type": "string", @@ -962,12 +958,12 @@ "additionalProperties": {}, "description": "JSON Schema for the canvas's open input. Opaque to AHP." }, - "actions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/SessionCanvasAction" + "$ref": "#/$defs/SessionCanvasOperation" }, - "description": "Actions this canvas exposes to the agent." + "description": "Operations this canvas exposes to the agent." } }, "required": [ @@ -978,27 +974,15 @@ }, "OpenCanvasRef": { "type": "object", - "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.", + "description": "A lightweight catalogue entry for one open canvas instance, surfaced on\n{@link SessionState.openCanvases}. The authoritative, mutable per-instance\nstate lives on the instance's own {@link CanvasState} channel; this entry\nexists so a subscriber can discover the channel URI and render it without\nsubscribing to every instance.\n\nThe instance is identified solely by its {@link channel} URI — the other\nidentity fields (`instanceId`, `providerId`) live on the full\n{@link CanvasState}, not here.", "properties": { - "instanceId": { - "type": "string", - "description": "Server-assigned instance handle, unique within the session." - }, "channel": { "$ref": "#/$defs/URI", - "description": "The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load\nthe full {@link CanvasState}." + "description": "The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the\nopen canvas; subscribe to it to load the full {@link CanvasState}." }, "canvasId": { "type": "string", - "description": "Provider-local canvas id this instance was opened from." - }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { - "type": "string", - "description": "Human-readable provider name." + "description": "Provider-local canvas id this instance was opened from. Retained so a\ncatalogue view can pick the right native renderer without subscribing." }, "title": { "type": "string", @@ -1010,10 +994,8 @@ } }, "required": [ - "instanceId", "channel", "canvasId", - "extensionId", "availability" ] }, @@ -4742,7 +4724,7 @@ }, "CanvasState": { "type": "object", - "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over this channel via `canvasReadResource` for an\n`ahp-canvas-content:` address. It never receives a \"render this\" request.", + "description": "Full state for a single open canvas instance, delivered when a client\nsubscribes to the instance's `ahp-canvas:/` channel.\n\nOne channel exists per open instance — the same \"one channel per resource\"\nconvention used by terminals, changesets, and resource watches. The\nlightweight catalogue entry that advertises this channel is\n{@link OpenCanvasRef} on {@link SessionState.openCanvases}; this state is the\nauthoritative, mutable per-instance view a renderer reads.\n\nRendering is state-driven: a client renders the canvas by reading\n{@link url} and resolving it per the renderer's URL policy — directly for a\nreachable address, or over the general `resourceRead` command for a\ncontent-reference address. It never receives a \"render this\" request.", "properties": { "instanceId": { "type": "string", @@ -4752,13 +4734,9 @@ "type": "string", "description": "Provider-local canvas id this instance was opened from." }, - "extensionId": { - "type": "string", - "description": "Owning provider id." - }, - "extensionName": { + "providerId": { "type": "string", - "description": "Human-readable provider name." + "description": "Owning provider id — an opaque namespace string AHP does not interpret,\ncarried so a provider-local {@link canvasId} stays unique across providers." }, "displayName": { "type": "string", @@ -4779,7 +4757,7 @@ }, "url": { "type": "string", - "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a channel-served `ahp-canvas-content://` address the\nrenderer resolves over this channel with `canvasReadResource`. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." + "description": "Renderer-targeted address for the opaque canvas content — either a\ndirectly-loadable URL (`https:`, an in-process scheme, `http://localhost`)\nor a content-reference URI the renderer resolves with the general\n`resourceRead` command. Resolving over `resourceRead` keeps content flowing\nentirely over the AHP transport, so a relayed or brokered deployment —\nwhere the host is reachable only over AHP and cannot be dialed directly —\ncan still serve every byte, with no port or direct connection required. The\nrenderer dispatches on the scheme and enforces its URL policy. See\n{@link /specification/canvas-channel | Canvas Channel}." }, "availability": { "$ref": "#/$defs/CanvasAvailability", @@ -4793,7 +4771,7 @@ "required": [ "instanceId", "canvasId", - "extensionId", + "providerId", "availability", "provider" ] diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 05736541..18851fbb 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -772,7 +772,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, - { name: 'SessionCanvasAction' }, + { name: 'SessionCanvasOperation' }, { name: 'SessionCanvasDeclaration' }, { name: 'ClientCanvasDeclaration' }, { name: 'OpenCanvasRef' }, @@ -1356,7 +1356,6 @@ const ACTION_VARIANTS: { { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, { type: 'canvas/updated', variantName: 'CanvasUpdated', tsInterface: 'CanvasUpdatedAction' }, { type: 'canvas/closeRequested', variantName: 'CanvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, - { type: 'canvas/message', variantName: 'CanvasMessage', tsInterface: 'CanvasMessageAction' }, ]; function generateMergedChatToolCallConfirmedStruct(): string { @@ -1493,10 +1492,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'InvokeChangesetOperationParams' }, { name: 'InvokeChangesetOperationResult' }, { name: 'ChangesetOperationFollowUp' }, { name: 'CanvasOpenParams' }, { name: 'CanvasOpenResult' }, - { name: 'CanvasInvokeActionParams' }, { name: 'CanvasInvokeActionResult' }, + { name: 'CanvasInvokeOperationParams' }, { name: 'CanvasInvokeOperationResult' }, { name: 'CanvasCloseParams' }, - { name: 'CanvasReadResourceParams' }, { name: 'CanvasReadResourceResult' }, - { name: 'CanvasResourceContent' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 97149797..0f8e91c0 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -854,7 +854,7 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', - 'SessionCanvasAction', 'SessionCanvasDeclaration', 'ClientCanvasDeclaration', 'OpenCanvasRef', + 'SessionCanvasOperation', 'SessionCanvasDeclaration', 'ClientCanvasDeclaration', 'OpenCanvasRef', 'CanvasServerProviderSource', 'CanvasClientProviderSource', 'CanvasState', ]; @@ -1270,7 +1270,6 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'resourceWatch/changed', caseName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, { type: 'canvas/updated', caseName: 'CanvasUpdated', tsInterface: 'CanvasUpdatedAction' }, { type: 'canvas/closeRequested', caseName: 'CanvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, - { type: 'canvas/message', caseName: 'CanvasMessage', tsInterface: 'CanvasMessageAction' }, ]; /** Merged data class for the approved/denied tool call confirmed action. */ @@ -1459,9 +1458,8 @@ const COMMAND_STRUCTS = [ 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', 'CanvasOpenParams', 'CanvasOpenResult', - 'CanvasInvokeActionParams', 'CanvasInvokeActionResult', + 'CanvasInvokeOperationParams', 'CanvasInvokeOperationResult', 'CanvasCloseParams', - 'CanvasReadResourceParams', 'CanvasReadResourceResult', 'CanvasResourceContent', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1700,7 +1698,7 @@ function generateErrorsFile(project: Project): string { lines.push(' const val ALREADY_EXISTS: Int = -32010'); lines.push(' /** An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state */'); lines.push(' const val CONFLICT: Int = -32011'); - lines.push(' /** A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` */'); + lines.push(' /** A canvas provider request (canvasOpen, canvasInvokeOperation, or canvasClose) failed; `data` carries a provider-defined `{ code, message }` */'); lines.push(' const val CANVAS_PROVIDER_ERROR: Int = -32012'); lines.push('}'); lines.push(''); diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 350e1294..20a9c664 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -803,7 +803,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, - { name: 'SessionCanvasAction' }, + { name: 'SessionCanvasOperation' }, { name: 'SessionCanvasDeclaration' }, { name: 'ClientCanvasDeclaration' }, { name: 'OpenCanvasRef' }, @@ -1263,7 +1263,6 @@ const ACTION_VARIANTS: { { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, { type: 'canvas/updated', variantName: 'CanvasUpdated', tsInterface: 'CanvasUpdatedAction' }, { type: 'canvas/closeRequested', variantName: 'CanvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, - { type: 'canvas/message', variantName: 'CanvasMessage', tsInterface: 'CanvasMessageAction' }, ]; function generateMergedToolCallConfirmedStruct(scope: 'Session' | 'Chat' = 'Session'): string { @@ -1437,10 +1436,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'InvokeChangesetOperationParams' }, { name: 'InvokeChangesetOperationResult' }, { name: 'ChangesetOperationFollowUp' }, { name: 'CanvasOpenParams' }, { name: 'CanvasOpenResult' }, - { name: 'CanvasInvokeActionParams' }, { name: 'CanvasInvokeActionResult' }, + { name: 'CanvasInvokeOperationParams' }, { name: 'CanvasInvokeOperationResult' }, { name: 'CanvasCloseParams' }, - { name: 'CanvasReadResourceParams' }, { name: 'CanvasReadResourceResult' }, - { name: 'CanvasResourceContent' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1663,7 +1660,7 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. \`ResourceWriteParams.if_match\`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; - /// A canvas provider request (\`canvasOpen\`, \`canvasInvokeAction\`, or \`canvasClose\`) failed; the error \`data\` carries a provider-defined \`{ code, message }\`. + /// A canvas provider request (\`canvasOpen\`, \`canvasInvokeOperation\`, or \`canvasClose\`) failed; the error \`data\` carries a provider-defined \`{ code, message }\`. pub const CANVAS_PROVIDER_ERROR: i32 = -32012; } diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index f622af1d..9cc24483 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -599,7 +599,7 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', - 'SessionCanvasAction', 'SessionCanvasDeclaration', 'ClientCanvasDeclaration', 'OpenCanvasRef', + 'SessionCanvasOperation', 'SessionCanvasDeclaration', 'ClientCanvasDeclaration', 'OpenCanvasRef', 'CanvasServerProviderSource', 'CanvasClientProviderSource', 'CanvasState', ]; @@ -1179,7 +1179,6 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'resourceWatch/changed', caseName: 'resourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, { type: 'canvas/updated', caseName: 'canvasUpdated', tsInterface: 'CanvasUpdatedAction' }, { type: 'canvas/closeRequested', caseName: 'canvasCloseRequested', tsInterface: 'CanvasCloseRequestedAction' }, - { type: 'canvas/message', caseName: 'canvasMessage', tsInterface: 'CanvasMessageAction' }, ]; /** Merged struct for the approved/denied tool call confirmed action */ @@ -1377,9 +1376,8 @@ const COMMAND_STRUCTS = [ 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', 'CanvasOpenParams', 'CanvasOpenResult', - 'CanvasInvokeActionParams', 'CanvasInvokeActionResult', + 'CanvasInvokeOperationParams', 'CanvasInvokeOperationResult', 'CanvasCloseParams', - 'CanvasReadResourceParams', 'CanvasReadResourceResult', 'CanvasResourceContent', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1611,7 +1609,7 @@ function generateErrorsFile(project: Project): string { lines.push(' public static let alreadyExists = -32010'); lines.push(' /// An optimistic-concurrency precondition failed: a request precondition token no longer matches the resource state'); lines.push(' public static let conflict = -32011'); - lines.push(' /// A canvas provider request (canvasOpen, canvasInvokeAction, or canvasClose) failed; `data` carries a provider-defined `{ code, message }`'); + lines.push(' /// A canvas provider request (canvasOpen, canvasInvokeOperation, or canvasClose) failed; `data` carries a provider-defined `{ code, message }`'); lines.push(' public static let canvasProviderError = -32012'); lines.push('}'); lines.push(''); diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 3d7703b5..95bf82c3 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -86,7 +86,6 @@ import type { SessionOpenCanvasesChangedAction, CanvasUpdatedAction, CanvasCloseRequestedAction, - CanvasMessageAction, } from './actions.js'; import { ActionType } from './actions.js'; @@ -346,14 +345,12 @@ export type ServerResourceWatchAction = export type CanvasAction = | CanvasUpdatedAction | CanvasCloseRequestedAction - | CanvasMessageAction ; /** Union of canvas actions that clients may dispatch. */ export type ClientCanvasAction = | CanvasUpdatedAction | CanvasCloseRequestedAction - | CanvasMessageAction ; /** Union of canvas actions that only the server may produce. */ @@ -451,5 +448,4 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.SessionOpenCanvasesChanged]: false, [ActionType.CanvasUpdated]: true, [ActionType.CanvasCloseRequested]: true, - [ActionType.CanvasMessage]: true, }; diff --git a/types/channels-canvas/actions.ts b/types/channels-canvas/actions.ts index e5bcbe38..e94f2187 100644 --- a/types/channels-canvas/actions.ts +++ b/types/channels-canvas/actions.ts @@ -60,24 +60,3 @@ export interface CanvasUpdatedAction { export interface CanvasCloseRequestedAction { type: ActionType.CanvasCloseRequested; } - -/** - * An opaque message relayed between the rendered canvas View and the - * instance's provider — the relay-carried analogue of a `postMessage` bridge. - * - * Bidirectional: a client dispatches it to carry a View→provider message, and - * the host emits it to carry a provider→View message (routed to the provider - * resolved for the instance, or handled host-internally for a server-side - * provider). Like `terminal/input` and {@link CanvasCloseRequestedAction} it - * is a pure signal with a no-op reducer, so it never bloats channel state. See - * {@link /specification/canvas-channel | Canvas Channel}. - * - * @category Canvas Actions - * @version 1 - * @clientDispatchable - */ -export interface CanvasMessageAction { - type: ActionType.CanvasMessage; - /** Opaque, provider-defined message payload. */ - payload: unknown; -} diff --git a/types/channels-canvas/commands.ts b/types/channels-canvas/commands.ts index 8046970f..07f1463c 100644 --- a/types/channels-canvas/commands.ts +++ b/types/channels-canvas/commands.ts @@ -1,7 +1,7 @@ /** * Canvas Channel Commands — the server → client provider request family - * (`canvasOpen` / `canvasInvokeAction` / `canvasClose`) and the client → - * server `canvasReadResource` content-fetch request. + * (`canvasOpen` / `canvasInvokeOperation` / `canvasClose`) that drives a + * client-declared canvas provider. * * @module channels-canvas/commands */ @@ -40,7 +40,7 @@ import type { BaseParams } from '../common/commands.js'; * "params": { * "channel": "ahp-session:/2f9c…", * "canvasId": "diff", - * "extensionId": "acme.canvases", + * "providerId": "acme.canvases", * "instanceId": "inst-7", * "input": { "path": "src/app.ts" } * } } @@ -57,8 +57,8 @@ export interface CanvasOpenParams extends BaseParams { channel: URI; /** Provider-local canvas id to open. */ canvasId: string; - /** Owning provider id. */ - extensionId: string; + /** Owning provider id (opaque to AHP). */ + providerId: string; /** Caller-minted handle for the new instance. */ instanceId: string; /** Open input, validated by the provider against its declared schema. */ @@ -77,45 +77,45 @@ export interface CanvasOpenResult { status?: string; } -// ─── canvasInvokeAction ────────────────────────────────────────────────────── +// ─── canvasInvokeOperation ─────────────────────────────────────────────────── /** - * Invokes one of a canvas's declared actions against its provider. + * Invokes one of a canvas's declared operations against its provider. * * Sent by the host to the providing client (or resolved host-internally for a * server-side provider) when the agent invokes a - * {@link SessionCanvasAction | declared action} on an open instance. The + * {@link SessionCanvasOperation | declared operation} on an open instance. The * provider returns an opaque, provider-defined value. Registered symmetrically * with the rest of the provider family (see {@link CanvasOpenParams}). * * @category Commands - * @method canvasInvokeAction + * @method canvasInvokeOperation * @direction Client ↔ Server * @messageType Request * @version 1 * @throws `CanvasProviderError` (`-32012`) if the provider has no handler for - * the action (`canvas_action_no_handler`) or the invocation fails; `data` - * carries the provider-defined `{ code, message }`. + * the operation or the invocation fails; `data` carries the provider-defined + * `{ code, message }`. */ -export interface CanvasInvokeActionParams extends BaseParams { +export interface CanvasInvokeOperationParams extends BaseParams { /** The owning session channel URI (`ahp-session:/`). */ channel: URI; - /** Instance handle the action targets. */ + /** Instance handle the operation targets. */ instanceId: string; /** Provider-local canvas id of the instance. */ canvasId: string; - /** Owning provider id. */ - extensionId: string; - /** Declared action name to invoke. */ - actionName: string; - /** Action input, validated by the provider against its declared schema. */ + /** Owning provider id (opaque to AHP). */ + providerId: string; + /** Declared operation name to invoke. */ + operationName: string; + /** Operation input, validated by the provider against its declared schema. */ input?: Record; } /** - * Result of the `canvasInvokeAction` command. + * Result of the `canvasInvokeOperation` command. */ -export interface CanvasInvokeActionResult { +export interface CanvasInvokeOperationResult { /** Opaque, provider-defined return value. */ value?: unknown; } @@ -144,78 +144,6 @@ export interface CanvasCloseParams extends BaseParams { instanceId: string; /** Provider-local canvas id of the instance. */ canvasId: string; - /** Owning provider id. */ - extensionId: string; -} - -// ─── canvasReadResource ────────────────────────────────────────────────────── - -/** - * Reads channel-served canvas content by `ahp-canvas-content:` URI. - * - * A client → host request, modeled on MCP's `resources/read`. When a canvas's - * {@link CanvasState.url} (or a sub-resource the rendered document references) - * is an `ahp-canvas-content://` address, the renderer cannot - * dial the host directly — for example in a relayed deployment behind a broker - * — so it resolves the bytes over the instance's existing `ahp-canvas:/` - * channel with this request instead of loading a network URL. The `` - * segment of the URI identifies which canvas channel to read from. See - * {@link /specification/canvas-channel | Canvas Channel}. - * - * @category Commands - * @method canvasReadResource - * @direction Client → Server - * @messageType Request - * @version 1 - * @throws `NotFound` (`-32008`) if the content URI does not resolve. - * @example - * ```jsonc - * // Client → Server - * { "jsonrpc": "2.0", "id": 52, "method": "canvasReadResource", - * "params": { - * "channel": "ahp-canvas:/9b1e…", - * "uri": "ahp-canvas-content:/inst-7/index.html" - * } } - * - * // Server → Client - * { "jsonrpc": "2.0", "id": 52, "result": { - * "contents": [ - * { "uri": "ahp-canvas-content:/inst-7/index.html", - * "mimeType": "text/html", "text": "…" } - * ] - * } } - * ``` - */ -export interface CanvasReadResourceParams extends BaseParams { - /** The owning canvas channel URI (`ahp-canvas:/`). */ - channel: URI; - /** An `ahp-canvas-content://` content URI to read. */ - uri: string; -} - -/** - * Result of the `canvasReadResource` command. - */ -export interface CanvasReadResourceResult { - /** The resolved content parts, wrapped for forward compatibility. */ - contents: CanvasResourceContent[]; -} - -/** - * One resolved piece of channel-served canvas content. - * - * Carries exactly one of {@link text} (text payloads) or {@link blob} - * (base64-encoded binary payloads). - * - * @category Commands - */ -export interface CanvasResourceContent { - /** The content URI this part resolves. */ - uri: string; - /** MIME type of the content, when known. */ - mimeType?: string; - /** UTF-8 text content, for text payloads. */ - text?: string; - /** Base64-encoded content, for binary payloads. */ - blob?: string; + /** Owning provider id (opaque to AHP). */ + providerId: string; } diff --git a/types/channels-canvas/reducer.ts b/types/channels-canvas/reducer.ts index a29b6022..8cce04bc 100644 --- a/types/channels-canvas/reducer.ts +++ b/types/channels-canvas/reducer.ts @@ -14,9 +14,9 @@ import { softAssertNever } from '../common/reducer-helpers.js'; * * `canvas/updated` is a sparse merge — a present field overwrites the * corresponding {@link CanvasState} field and an absent field preserves the - * current value. `canvas/closeRequested` and `canvas/message` are pure - * signals with no state effect (the host acts on them out of band), mirroring - * how `terminal/input` is side-effect-only. + * current value. `canvas/closeRequested` is a pure signal with no state effect + * (the host acts on it out of band), mirroring how `terminal/input` is + * side-effect-only. */ export function canvasReducer(state: CanvasState, action: CanvasAction, log?: (msg: string) => void): CanvasState { switch (action.type) { @@ -34,10 +34,6 @@ export function canvasReducer(state: CanvasState, action: CanvasAction, log?: (m // the close flow; the reducer keeps no state for it. return state; - case ActionType.CanvasMessage: - // Side-effect-only: an opaque View↔provider relay message. No state. - return state; - default: softAssertNever(action, log); return state; diff --git a/types/channels-canvas/state.ts b/types/channels-canvas/state.ts index 8a45c1df..bc5d3f9c 100644 --- a/types/channels-canvas/state.ts +++ b/types/channels-canvas/state.ts @@ -24,8 +24,8 @@ import type { * * Rendering is state-driven: a client renders the canvas by reading * {@link url} and resolving it per the renderer's URL policy — directly for a - * reachable address, or over this channel via `canvasReadResource` for an - * `ahp-canvas-content:` address. It never receives a "render this" request. + * reachable address, or over the general `resourceRead` command for a + * content-reference address. It never receives a "render this" request. * * @category Canvas State */ @@ -34,10 +34,11 @@ export interface CanvasState { instanceId: string; /** Provider-local canvas id this instance was opened from. */ canvasId: string; - /** Owning provider id. */ - extensionId: string; - /** Human-readable provider name. */ - extensionName?: string; + /** + * Owning provider id — an opaque namespace string AHP does not interpret, + * carried so a provider-local {@link canvasId} stays unique across providers. + */ + providerId: string; /** Human-readable canvas name. */ displayName?: string; /** @@ -52,8 +53,11 @@ export interface CanvasState { /** * Renderer-targeted address for the opaque canvas content — either a * directly-loadable URL (`https:`, an in-process scheme, `http://localhost`) - * or a channel-served `ahp-canvas-content://` address the - * renderer resolves over this channel with `canvasReadResource`. The + * or a content-reference URI the renderer resolves with the general + * `resourceRead` command. Resolving over `resourceRead` keeps content flowing + * entirely over the AHP transport, so a relayed or brokered deployment — + * where the host is reachable only over AHP and cannot be dialed directly — + * can still serve every byte, with no port or direct connection required. The * renderer dispatches on the scheme and enforces its URL policy. See * {@link /specification/canvas-channel | Canvas Channel}. */ diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 737ac9b8..f268e547 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -252,8 +252,8 @@ export interface SessionActiveClient { * {@link SessionState.canvases} with * {@link SessionCanvasDeclaration.source | `source`} set to * `{ kind: 'client', clientId }` and routes - * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for them back - * to this client. Only meaningful for a client that declared + * `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for them + * back to this client. Only meaningful for a client that declared * {@link ClientCapabilities.canvas}. */ canvasProviders?: ClientCanvasDeclaration[]; @@ -301,8 +301,8 @@ export interface CanvasServerProviderSource { /** * A canvas provided by a connected client. The host routes - * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for this canvas - * to the identified client. + * `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for this + * canvas to the identified client. * * @category Canvas Types */ @@ -325,19 +325,20 @@ export type CanvasProviderSource = | CanvasClientProviderSource; /** - * One named action a canvas exposes to the agent, mirroring the shape of a + * One named operation a canvas exposes to the agent, mirroring the shape of a * {@link ToolDefinition} entry. Unique within its owning - * `(extensionId, canvasId)`. + * `(providerId, canvasId)`. Named "operation" to match the changeset channel's + * operation vocabulary. * * @category Canvas Types */ -export interface SessionCanvasAction { - /** Action name, unique within the owning `(extensionId, canvasId)`. */ +export interface SessionCanvasOperation { + /** Operation name, unique within the owning `(providerId, canvasId)`. */ name: string; - /** Human-readable description of what the action does. */ + /** Human-readable description of what the operation does. */ description?: string; /** - * JSON Schema for the action's input. Opaque to AHP; mirrors the + * JSON Schema for the operation's input. Opaque to AHP; mirrors the * {@link ToolDefinition.inputSchema} shape. */ inputSchema?: Record; @@ -350,11 +351,13 @@ export interface SessionCanvasAction { * @category Canvas Types */ export interface SessionCanvasDeclaration { - /** Owning provider id. Stable across declarations and instances. */ - extensionId: string; - /** Human-readable provider name. */ - extensionName?: string; - /** Provider-local canvas id. Unique within `extensionId`. */ + /** + * Owning provider id — an opaque namespace string AHP does not interpret, + * carried so a provider-local {@link canvasId} stays unique across providers. + * Stable across declarations and instances. + */ + providerId: string; + /** Provider-local canvas id. Unique within `providerId`. */ canvasId: string; /** Human-readable canvas name. */ displayName: string; @@ -365,8 +368,8 @@ export interface SessionCanvasDeclaration { * {@link ToolDefinition.inputSchema} shape. */ inputSchema?: Record; - /** Actions this canvas exposes to the agent. */ - actions?: SessionCanvasAction[]; + /** Operations this canvas exposes to the agent. */ + operations?: SessionCanvasOperation[]; /** Where the declaration came from — for routing and cleanup. */ source: CanvasProviderSource; } @@ -374,7 +377,7 @@ export interface SessionCanvasDeclaration { /** * The lighter declaration shape a client publishes on * {@link SessionActiveClient.canvasProviders}. The host derives the - * `extensionId` and {@link SessionCanvasDeclaration.source | `source`} when + * `providerId` and {@link SessionCanvasDeclaration.source | `source`} when * folding it into {@link SessionState.canvases}. * * @category Canvas Types @@ -388,8 +391,8 @@ export interface ClientCanvasDeclaration { description: string; /** JSON Schema for the canvas's open input. Opaque to AHP. */ inputSchema?: Record; - /** Actions this canvas exposes to the agent. */ - actions?: SessionCanvasAction[]; + /** Operations this canvas exposes to the agent. */ + operations?: SessionCanvasOperation[]; } /** @@ -399,22 +402,23 @@ export interface ClientCanvasDeclaration { * exists so a subscriber can discover the channel URI and render it without * subscribing to every instance. * + * The instance is identified solely by its {@link channel} URI — the other + * identity fields (`instanceId`, `providerId`) live on the full + * {@link CanvasState}, not here. + * * @category Canvas Types */ export interface OpenCanvasRef { - /** Server-assigned instance handle, unique within the session. */ - instanceId: string; /** - * The instance's channel URI (`ahp-canvas:/`). Subscribe to it to load - * the full {@link CanvasState}. + * The instance's channel URI (`ahp-canvas:/`). Uniquely identifies the + * open canvas; subscribe to it to load the full {@link CanvasState}. */ channel: URI; - /** Provider-local canvas id this instance was opened from. */ + /** + * Provider-local canvas id this instance was opened from. Retained so a + * catalogue view can pick the right native renderer without subscribing. + */ canvasId: string; - /** Owning provider id. */ - extensionId: string; - /** Human-readable provider name. */ - extensionName?: string; /** Current instance title, mirrored from {@link CanvasState.title}. */ title?: string; /** Whether the instance's provider is currently available. */ diff --git a/types/common/actions.ts b/types/common/actions.ts index ed9d5f64..b6554576 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -113,7 +113,6 @@ import type { import type { CanvasUpdatedAction, CanvasCloseRequestedAction, - CanvasMessageAction, } from '../channels-canvas/actions.js'; // ─── Action Type Enum ──────────────────────────────────────────────────────── @@ -207,7 +206,6 @@ export const enum ActionType { SessionOpenCanvasesChanged = 'session/openCanvasesChanged', CanvasUpdated = 'canvas/updated', CanvasCloseRequested = 'canvas/closeRequested', - CanvasMessage = 'canvas/message', } // ─── Action Envelope ───────────────────────────────────────────────────────── @@ -326,5 +324,4 @@ export type StateAction = | SessionCanvasesChangedAction | SessionOpenCanvasesChangedAction | CanvasUpdatedAction - | CanvasCloseRequestedAction - | CanvasMessageAction; + | CanvasCloseRequestedAction; diff --git a/types/common/commands.ts b/types/common/commands.ts index 554d31ed..2a08f3e5 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -208,7 +208,7 @@ export interface ClientCapabilities { /** * Client can render canvases and host client-declared canvas providers — it * can render an opaque canvas URL in an isolated surface, and it can answer - * `canvasOpen` / `canvasInvokeAction` / `canvasClose` requests for canvases + * `canvasOpen` / `canvasInvokeOperation` / `canvasClose` requests for canvases * it declares via {@link SessionActiveClient.canvasProviders}. * * Hosts SHOULD only populate {@link SessionState.canvases} / diff --git a/types/common/errors.ts b/types/common/errors.ts index 736f75a1..c6f41321 100644 --- a/types/common/errors.ts +++ b/types/common/errors.ts @@ -96,12 +96,13 @@ export const AhpErrorCodes = { */ Conflict: -32011, /** - * A canvas provider request (`canvasOpen`, `canvasInvokeAction`, or + * A canvas provider request (`canvasOpen`, `canvasInvokeOperation`, or * `canvasClose`) failed. The `data` field of the JSON-RPC error MUST be a * {@link CanvasProviderErrorData} carrying the provider-defined * `{ code, message }` — for example a `canvas_action_no_handler` code when * the provider declared the canvas but has no handler for the requested - * action, or `canvas_provider_unavailable` when the provider is disconnected. + * operation, or `canvas_provider_unavailable` when the provider is + * disconnected. */ CanvasProviderError: -32012, } as const; diff --git a/types/common/messages.ts b/types/common/messages.ts index f5ac6ddd..d32e3966 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -73,11 +73,9 @@ import type { import type { CanvasOpenParams, CanvasOpenResult, - CanvasInvokeActionParams, - CanvasInvokeActionResult, + CanvasInvokeOperationParams, + CanvasInvokeOperationResult, CanvasCloseParams, - CanvasReadResourceParams, - CanvasReadResourceResult, } from '../channels-canvas/commands.js'; import type { ActionEnvelope } from './actions.js'; @@ -184,9 +182,8 @@ export interface CommandMap { 'completions': { params: CompletionsParams; result: CompletionsResult }; 'invokeChangesetOperation': { params: InvokeChangesetOperationParams; result: InvokeChangesetOperationResult }; 'canvasOpen': { params: CanvasOpenParams; result: CanvasOpenResult }; - 'canvasInvokeAction': { params: CanvasInvokeActionParams; result: CanvasInvokeActionResult }; + 'canvasInvokeOperation': { params: CanvasInvokeOperationParams; result: CanvasInvokeOperationResult }; 'canvasClose': { params: CanvasCloseParams; result: null }; - 'canvasReadResource': { params: CanvasReadResourceParams; result: CanvasReadResourceResult }; } /** @@ -201,7 +198,7 @@ export interface CommandMap { * `virtual://my-client/...` plugins) and to drive per-session filesystem * providers without the client having to re-implement the wire schema. * - * The `canvas*` provider family (`canvasOpen` / `canvasInvokeAction` / + * The `canvas*` provider family (`canvasOpen` / `canvasInvokeOperation` / * `canvasClose`) is server → client: the host drives a client-declared * canvas provider. It is mirrored in {@link CommandMap} for symmetry with * the `resource*` precedent; a client normally never initiates it, and a @@ -222,7 +219,7 @@ export interface ServerCommandMap { 'resourceRequest': { params: ResourceRequestParams; result: ResourceRequestResult }; 'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult }; 'canvasOpen': { params: CanvasOpenParams; result: CanvasOpenResult }; - 'canvasInvokeAction': { params: CanvasInvokeActionParams; result: CanvasInvokeActionResult }; + 'canvasInvokeOperation': { params: CanvasInvokeOperationParams; result: CanvasInvokeOperationResult }; 'canvasClose': { params: CanvasCloseParams; result: null }; } diff --git a/types/index.ts b/types/index.ts index 600d47e0..95cbb705 100644 --- a/types/index.ts +++ b/types/index.ts @@ -112,7 +112,7 @@ export type { ResourceWatchState, ResourceChange, CanvasState, - SessionCanvasAction, + SessionCanvasOperation, SessionCanvasDeclaration, ClientCanvasDeclaration, OpenCanvasRef, @@ -229,7 +229,6 @@ export type { SessionOpenCanvasesChangedAction, CanvasUpdatedAction, CanvasCloseRequestedAction, - CanvasMessageAction, } from './actions.js'; export { ActionType } from './actions.js'; @@ -346,12 +345,9 @@ export type { ChangesetOperationFollowUp, CanvasOpenParams, CanvasOpenResult, - CanvasInvokeActionParams, - CanvasInvokeActionResult, + CanvasInvokeOperationParams, + CanvasInvokeOperationResult, CanvasCloseParams, - CanvasReadResourceParams, - CanvasReadResourceResult, - CanvasResourceContent, } from './commands.js'; export { ReconnectResultType, ContentEncoding, CompletionItemKind, ResourceType, ResourceWriteMode } from './commands.js'; diff --git a/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json b/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json index dca3d0b5..f9210433 100644 --- a/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json +++ b/types/test-cases/reducers/232-session-canvaseschanged-sets-registry.json @@ -14,19 +14,18 @@ "type": "session/canvasesChanged", "canvases": [ { - "extensionId": "acme.editors", - "extensionName": "Acme Editors", + "providerId": "acme.editors", "canvasId": "markdown", "displayName": "Markdown Editor", "description": "Edit and preview Markdown documents.", "source": { "kind": "server" } }, { - "extensionId": "acme.viewers", + "providerId": "acme.viewers", "canvasId": "image", "displayName": "Image Viewer", "description": "Preview raster and vector images.", - "actions": [ + "operations": [ { "name": "zoom", "description": "Zoom the image to a factor." } ], "source": { "kind": "client", "clientId": "client-42" } @@ -41,19 +40,18 @@ "lifecycle": "ready", "canvases": [ { - "extensionId": "acme.editors", - "extensionName": "Acme Editors", + "providerId": "acme.editors", "canvasId": "markdown", "displayName": "Markdown Editor", "description": "Edit and preview Markdown documents.", "source": { "kind": "server" } }, { - "extensionId": "acme.viewers", + "providerId": "acme.viewers", "canvasId": "image", "displayName": "Image Viewer", "description": "Preview raster and vector images.", - "actions": [ + "operations": [ { "name": "zoom", "description": "Zoom the image to a factor." } ], "source": { "kind": "client", "clientId": "client-42" } diff --git a/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json b/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json index 51d94580..9fba06e3 100644 --- a/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json +++ b/types/test-cases/reducers/233-session-opencanvaseschanged-sets-catalogue.json @@ -14,19 +14,14 @@ "type": "session/openCanvasesChanged", "openCanvases": [ { - "instanceId": "canvas-1", "channel": "ahp-canvas:/canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", - "extensionName": "Acme Editors", "title": "README.md", "availability": "ready" }, { - "instanceId": "canvas-2", "channel": "ahp-canvas:/canvas-2", "canvasId": "image", - "extensionId": "acme.viewers", "availability": "stale" } ] @@ -39,19 +34,14 @@ "lifecycle": "ready", "openCanvases": [ { - "instanceId": "canvas-1", "channel": "ahp-canvas:/canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", - "extensionName": "Acme Editors", "title": "README.md", "availability": "ready" }, { - "instanceId": "canvas-2", "channel": "ahp-canvas:/canvas-2", "canvasId": "image", - "extensionId": "acme.viewers", "availability": "stale" } ], diff --git a/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json b/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json index db2ac17d..9b1edae4 100644 --- a/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json +++ b/types/test-cases/reducers/234-canvas-updated-merges-all-fields.json @@ -4,12 +4,11 @@ "initial": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", - "extensionName": "Acme Editors", + "providerId": "acme.editors", "displayName": "Markdown Editor", "title": "Draft", "status": "idle", - "url": "ahp-canvas-content:/canvas-1/index.html", + "url": "ahp-session:/2f9c/content/canvas-1", "availability": "ready", "provider": { "kind": "server" } }, @@ -25,8 +24,7 @@ "expected": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", - "extensionName": "Acme Editors", + "providerId": "acme.editors", "displayName": "Markdown Editor", "title": "Published", "status": "saved", diff --git a/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json b/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json index a26256f9..1d4997bb 100644 --- a/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json +++ b/types/test-cases/reducers/235-canvas-updated-partial-preserves-absent.json @@ -4,10 +4,10 @@ "initial": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "title": "Draft", "status": "idle", - "url": "ahp-canvas-content:/canvas-1/index.html", + "url": "ahp-session:/2f9c/content/canvas-1", "availability": "ready", "provider": { "kind": "client", "clientId": "client-42" } }, @@ -21,7 +21,7 @@ "expected": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "title": "Renamed", "status": "idle", "url": "https://example.com/docs/renamed.html", diff --git a/types/test-cases/reducers/236-canvas-updated-partial-complementary.json b/types/test-cases/reducers/236-canvas-updated-partial-complementary.json index 17419321..835ee30b 100644 --- a/types/test-cases/reducers/236-canvas-updated-partial-complementary.json +++ b/types/test-cases/reducers/236-canvas-updated-partial-complementary.json @@ -4,10 +4,10 @@ "initial": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "title": "Draft", "status": "idle", - "url": "ahp-canvas-content:/canvas-1/index.html", + "url": "ahp-session:/2f9c/content/canvas-1", "availability": "ready", "provider": { "kind": "server" } }, @@ -21,10 +21,10 @@ "expected": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "title": "Draft", "status": "error", - "url": "ahp-canvas-content:/canvas-1/index.html", + "url": "ahp-session:/2f9c/content/canvas-1", "availability": "stale", "provider": { "kind": "server" } } diff --git a/types/test-cases/reducers/237-canvas-closerequested-no-op.json b/types/test-cases/reducers/237-canvas-closerequested-no-op.json index ef06ab56..bc8737ac 100644 --- a/types/test-cases/reducers/237-canvas-closerequested-no-op.json +++ b/types/test-cases/reducers/237-canvas-closerequested-no-op.json @@ -4,7 +4,7 @@ "initial": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "title": "Draft", "availability": "ready", "provider": { "kind": "server" } @@ -17,7 +17,7 @@ "expected": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "title": "Draft", "availability": "ready", "provider": { "kind": "server" } diff --git a/types/test-cases/reducers/238-canvas-message-no-op.json b/types/test-cases/reducers/238-canvas-message-no-op.json deleted file mode 100644 index b340763e..00000000 --- a/types/test-cases/reducers/238-canvas-message-no-op.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "description": "canvas/message is a side-effect-only relay signal — the reducer keeps state unchanged", - "reducer": "canvas", - "initial": { - "instanceId": "canvas-1", - "canvasId": "markdown", - "extensionId": "acme.editors", - "title": "Draft", - "availability": "ready", - "provider": { "kind": "client", "clientId": "client-42" } - }, - "actions": [ - { - "type": "canvas/message", - "payload": { "kind": "scrollTo", "line": 120 } - } - ], - "expected": { - "instanceId": "canvas-1", - "canvasId": "markdown", - "extensionId": "acme.editors", - "title": "Draft", - "availability": "ready", - "provider": { "kind": "client", "clientId": "client-42" } - } -} diff --git a/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json b/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json index d21e3de6..74e1763b 100644 --- a/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json +++ b/types/test-cases/reducers/239-canvas-unknown-action-type-is-no-op.json @@ -4,7 +4,7 @@ "initial": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "availability": "ready", "provider": { "kind": "server" } }, @@ -16,7 +16,7 @@ "expected": { "instanceId": "canvas-1", "canvasId": "markdown", - "extensionId": "acme.editors", + "providerId": "acme.editors", "availability": "ready", "provider": { "kind": "server" } } diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index e93cad05..6e55005c 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -78,9 +78,8 @@ type _ExpectedCommands = | 'completions' | 'invokeChangesetOperation' | 'canvasOpen' - | 'canvasInvokeAction' - | 'canvasClose' - | 'canvasReadResource'; + | 'canvasInvokeOperation' + | 'canvasClose'; /** All methods annotated `@messageType Notification` (client → server). */ type _ExpectedClientNotifications = @@ -112,7 +111,7 @@ type _ExpectedServerCommands = | 'resourceRequest' | 'createResourceWatch' | 'canvasOpen' - | 'canvasInvokeAction' + | 'canvasInvokeOperation' | 'canvasClose'; // ─── Assertions ────────────────────────────────────────────────────────────── diff --git a/types/version/registry.ts b/types/version/registry.ts index 3893b359..c81c8aeb 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -160,7 +160,6 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionOpenCanvasesChanged]: '0.6.0', [ActionType.CanvasUpdated]: '0.6.0', [ActionType.CanvasCloseRequested]: '0.6.0', - [ActionType.CanvasMessage]: '0.6.0', }; /**