Skip to content

canvas: expose registry state and URI-addressed open - #528

Draft
Joaquín Ruales (jruales) wants to merge 4 commits into
mainfrom
jruales/ahp-canvas-inventory
Draft

Joaquín Ruales (jruales) wants to merge 4 commits into
mainfrom
jruales/ahp-canvas-inventory

Conversation

@jruales

@jruales Joaquín Ruales (jruales) commented Oct 14, 2026 •

Copy link
Copy Markdown

Summary

AHP already projects live Canvas instances through ChatState.canvases and experimental ahp-canvas: channels, but clients also need an authoritative way to discover the Canvas types available to one exact chat and open a selected type.

This PR models discovery as chat state rather than a point-in-time list command:

  • ChatState.canvasRegistry is the authoritative exact-chat Canvas type registry.
  • chat/canvasRegistryChanged replaces or clears that registry when live provider registration changes.
  • Client-dispatchable chat/canvasRegistryRequested explicitly requests potentially effectful materialization for a persisted cold chat.
  • openCanvas uses a client-chosen ahp-canvas: URI as the AHP instance identity and separately identifies the owning chat and declaration.
  • Extension/provider metadata is normalized once per CanvasExtensionDeclaration; each nested CanvasTypeDeclaration contains only type-local metadata.
  • The discovery surface remains Stability 1 — Experimental and capability-gated by AgentCapabilities.canvases.

Protocol behavior

Live registry state

For a live chat, the host keeps canvasRegistry synchronized with provider registration and emits chat/canvasRegistryChanged only when the authoritative registry changes. Clients do not poll and do not treat changes to the open-instance list as registry invalidation.

The registry distinguishes three states:

  • absent: not materialized or currently unavailable;
  • { extensions: [] }: materialized, with no available Canvas types;
  • populated: declarations keyed by (extensionId, canvasId).

extensionId is opaque. Clients must not infer source or management behavior by parsing it. requiresInput remains so clients can exclude declarations whose input flow they do not support; the previous speculative actionCount field is removed.

Cold materialization

When a persisted chat is cold, a client may dispatch chat/canvasRegistryRequested. The reducer intentionally leaves optimistic state unchanged while the host may resume the backing runtime, discover extensions, and execute extension startup code. The host then publishes the complete result through chat/canvasRegistryChanged.

Materialization must not create a backing for a brand-new chat, send a user message, or invoke a model.

URI-addressed opening

openCanvas.channel is the client-chosen ahp-canvas: resource URI. Reusing that URI for the same chat and declaration focuses or refreshes the logical instance; reusing it for a different chat, extension, or Canvas type must fail with Conflict.

CanvasState.instanceId remains provider-facing correlation metadata. The Canvas URI is the AHP resource identity.

sequenceDiagram
    participant C as Client
    participant Chat as Exact chat channel
    participant H as Agent Host
    participant Canvas as ahp-canvas resource

    C->>Chat: subscribe
    Chat-->>C: snapshot with canvasRegistry or no registry
    opt Registry is absent and the chat is persisted
        C->>Chat: dispatch canvasRegistryRequested
        H->>H: resume runtime and discover providers
        H-->>Chat: canvasRegistryChanged(full registry)
        Chat-->>C: authoritative registry action
    end
    C->>H: openCanvas(channel, chat, extensionId, canvasId)
    H-->>Chat: canvasesChanged(CanvasReference)
    C->>Canvas: subscribe(reference.resource)
    Canvas-->>C: CanvasState
Loading

Compatibility and scope

  • The change is additive and capability-gated; hosts that omit AgentCapabilities.canvases remain unsupported without probing.
  • The current development protocol remains 1.1.0; both registry actions are registered at 1.1.0.
  • Existing server-published Canvas references and channels remain the live-instance mechanism.
  • This PR defines protocol state, actions, the open request, schemas, documentation, and generated clients. It does not implement host/runtime behavior, client UI, extension installation or enablement, input-bearing Canvas flows, or pre-session catalogue probing.

Validation

Check Result
RUSTUP_TOOLCHAIN=stable npm run generate Passed; all generated surfaces updated
npm test 557 passed; 100% reducer coverage; generated output verified
RUSTUP_TOOLCHAIN=stable npm run docs:build Passed
TypeScript npm test && npm run build 77 passed; build passed
Rust format, workspace tests, and doc tests Passed
Go format, vet, and tests Passed
swift test Passed
Kotlin tests on JDK 17 Passed
.NET format, Release build, and tests Passed; 639 tests

🤖 Written by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Comment thread types/channels-canvas/commands.ts Outdated
Comment on lines +17 to +46
/**
* Lists canvas types currently available to open for one exact chat.
*
* This read MAY lazily resume an existing persisted chat so its extension
* registry can be reconstructed. It MUST NOT create a backing runtime for a
* brand-new chat, send a user message, or invoke a model.
*
* Clients MUST NOT call this command unless the selected agent advertises
* {@link AgentCapabilities.canvases}.
*
* @category Commands
* @method listCanvasTypes
* @direction Client → Server
* @messageType Request
* @version 1
*/
export interface ListCanvasTypesParams extends BaseParams {
/** Exact chat URI whose extension registry should be queried. */
channel: URI;
}

/**
* Canvas types available to open for the requested chat.
*
* @category Commands
*/
export interface ListCanvasTypesResult {
/** Available canvas types and their declaring extensions. */
items: CanvasTypeDeclaration[];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is enumerating canvas types expensive? If not I would expect these to be on a subscribable state object somewhere so they update in realtime.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I changed this from an imperative listCanvasTypes snapshot to authoritative exact-chat state. ChatState.canvasRegistry is kept current through full chat/canvasRegistryChanged replacements, so clients do not poll or use live-instance changes as registry invalidation.

Cold materialization is separated because it can resume the backing runtime and execute extension startup code: a client explicitly dispatches chat/canvasRegistryRequested, whose reducer is a no-op, and the host publishes the resulting registry through chat/canvasRegistryChanged.

🤖 Written by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Auto-replied by the GitHub Copilot app

Comment thread types/channels-canvas/commands.ts Outdated
* @version 1
*/
export interface OpenCanvasParams extends BaseParams {
/** Exact chat URI that owns the canvas instance. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this is an ahp-canvas channel

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. openCanvas.channel is now the client-chosen ahp-canvas: resource URI. The exact owning chat is passed separately in chat, and authoritative membership and presentation continue through ChatState.canvases plus subscription to that Canvas channel.

🤖 Written by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Auto-replied by the GitHub Copilot app

Comment thread types/channels-canvas/commands.ts Outdated
extensionId: string;
/** Provider-local canvas type identifier. */
canvasId: string;
/** Caller-chosen stable instance identifier. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this in addition to the URI?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the client-supplied instanceId from OpenCanvasParams; the client-chosen ahp-canvas: URI is now the AHP resource identity. Reusing the URI for the same chat and declaration focuses or refreshes that logical instance, while conflicting reuse must fail.

The existing CanvasState.instanceId remains only as provider-facing correlation metadata and is documented as not being a second AHP identity.

🤖 Written by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Auto-replied by the GitHub Copilot app

Comment thread types/channels-canvas/state.ts Outdated
Comment on lines +56 to +59
/** Whether opening this canvas requires an input flow. */
requiresInput: boolean;
/** Number of actions the canvas declares. */
actionCount: number;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these get used by the UI?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced these through the current UI consumers. requiresInput is used by the Add Tab/Open Canvas UI to exclude declarations that need an input flow the menu cannot provide, so I retained it. actionCount had no active consumer, and a count would not be sufficient for future action invocation anyway, so I removed it.

🤖 Written by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Auto-replied by the GitHub Copilot app

Comment thread types/channels-canvas/state.ts Outdated
Comment on lines +46 to +51
/** Stable identifier of the extension that declares the canvas. */
extensionId: string;
/** Where the runtime discovered the declaring extension. */
extensionSource: CanvasExtensionSource;
/** Human-readable extension name, when available. */
extensionName?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does a UI do with these things? I doubt we should denormalize extensionSource/extensionName into individual canvas declarations

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I normalized this into CanvasRegistry.extensions: each CanvasExtensionDeclaration carries extensionId, source, and display name once, with its type-local CanvasTypeDeclaration[] nested beneath it.

The UI uses the extension name to disambiguate duplicate Canvas display names and the explicit source as contribution provenance/openability metadata. extensionId is now documented as opaque, so clients must not infer source or management behavior by parsing it.

🤖 Written by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Auto-replied by the GitHub Copilot app

@jruales Joaquín Ruales (jruales) changed the title canvas: add discovery and open commands canvas: expose registry state and URI-addressed open Oct 15, 2026
Add capability-gated exact-chat listCanvasTypes and openCanvas commands, including owning extension metadata, and regenerate all protocol clients and schemas.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2
Replace snapshot-only Canvas listing with an authoritative exact-chat registry, explicit cold materialization action, and URI-addressed Canvas opening. Normalize provider metadata and update every generated client, reducer, fixture, and specification.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0a6aa762-4b80-4e33-be5d-68d9f3398af2

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants