Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public ActionType(string value)

public static readonly ActionType ChatCanvasesChanged = new ActionType("chat/canvasesChanged");

public static readonly ActionType ChatCanvasRegistryRequested = new ActionType("chat/canvasRegistryRequested");

public static readonly ActionType ChatCanvasRegistryChanged = new ActionType("chat/canvasRegistryChanged");

public static readonly ActionType CanvasStateChanged = new ActionType("canvas/stateChanged");

public static readonly ActionType ChatWorkingDirectorySet = new ActionType("chat/workingDirectorySet");
Expand Down Expand Up @@ -1847,6 +1851,39 @@ public sealed record ChatCanvasesChangedAction
public List<CanvasReference>? Canvases { get; init; }
}

/// <summary>Requests materialization of the chat's Canvas type registry.
///
/// A host MUST reject the action when the chat's agent does not advertise
/// {@link AgentCapabilities.canvases} or when the chat cannot materialize the
/// registry. Materialization MUST NOT send a user message or invoke a model.
///
/// The reducer is a no-op. The host remains authoritative and publishes the
/// complete result through
/// {@link ChatCanvasRegistryChangedAction | `chat/canvasRegistryChanged`}.
/// Clients do not need to dispatch this action when
/// {@link ChatState.canvasRegistry} is already present.
///
/// Stability: 1.0 - Early development.</summary>
public sealed record ChatCanvasRegistryRequestedAction
{
public ActionType Type { get; init; } = ActionType.ChatCanvasRegistryRequested;
}

/// <summary>The Canvas type registry for this chat changed.
///
/// Replaces {@link ChatState.canvasRegistry | `state.canvasRegistry`} entirely.
/// Set to `undefined` when the registry is no longer materialized or available.
///
/// Stability: 1.0 - Early development.</summary>
public sealed record ChatCanvasRegistryChangedAction
{
public ActionType Type { get; init; } = ActionType.ChatCanvasRegistryChanged;

/// <summary>Authoritative registry, or `undefined` to clear it.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CanvasRegistry? CanvasRegistry { get; init; }
}

/// <summary>The presentation state for this canvas changed.
///
/// Replaces the subscribed canvas channel state entirely. Full-replacement
Expand Down Expand Up @@ -2922,6 +2959,8 @@ public StateActionConverter()
["chat/movableChanged"] = typeof(ChatMovableChangedAction),
["chat/changesetsChanged"] = typeof(ChatChangesetsChangedAction),
["chat/canvasesChanged"] = typeof(ChatCanvasesChangedAction),
["chat/canvasRegistryRequested"] = typeof(ChatCanvasRegistryRequestedAction),
["chat/canvasRegistryChanged"] = typeof(ChatCanvasRegistryChangedAction),
["canvas/stateChanged"] = typeof(CanvasStateChangedAction),
["chat/workingDirectorySet"] = typeof(ChatWorkingDirectorySetAction),
["chat/workingDirectoryRemoved"] = typeof(ChatWorkingDirectoryRemovedAction),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,64 @@ public sealed record MoveChatResult
public required string Session { get; init; }
}

/// <summary>Opens one client-addressed canvas resource in a chat.
///
/// The resulting live instance is published through the chat's
/// {@link ChatState.canvases} references and its `ahp-canvas:` state channel.
/// Repeating this command with the same `channel` is idempotent. Reusing it for
/// a different chat, extension, or canvas type MUST fail with `Conflict`.
/// Clients MUST observe
/// {@link ChatState.canvases} and the referenced `ahp-canvas:` channel for
/// authoritative membership, availability, and presentation state.
///
/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.</summary>
public sealed record OpenCanvasParams
{
/// <summary>Client-chosen URI for the canvas resource, for example
/// `ahp-canvas:/&lt;uuid&gt;`.</summary>
public required string Channel { get; init; }

/// <summary>Optional JSON-serializable metadata associated with this request.
/// Receivers MUST ignore keys they do not understand.</summary>
[JsonPropertyName("_meta")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, JsonElement>? Meta { get; init; }

/// <summary>Exact chat URI that owns the canvas instance.</summary>
public required string Chat { get; init; }

/// <summary>Stable identifier of the extension that declares the canvas.</summary>
public required string ExtensionId { get; init; }

/// <summary>Provider-local canvas type identifier.</summary>
public required string CanvasId { get; init; }
}

/// <summary>Closes and disposes one live canvas resource.
///
/// The server removes the resource from the owning chat's
/// {@link ChatState.canvases} references. Repeating this command after the
/// resource is gone is a no-op.
///
/// Clients MUST NOT call this command unless the chat's agent advertises
/// {@link AgentCapabilities.canvases}.
///
/// Stability: 1.0 - Early development.</summary>
public sealed record DisposeCanvasParams
{
/// <summary>URI of the canvas resource to dispose.</summary>
public required string Channel { get; init; }

/// <summary>Optional JSON-serializable metadata associated with this request.
/// Receivers MUST ignore keys they do not understand.</summary>
[JsonPropertyName("_meta")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, JsonElement>? Meta { get; init; }
}

/// <summary>Returns a list of session summaries. Used to populate session lists and sidebars.
///
/// The session list is **not** part of the state tree because it can be arbitrarily
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,13 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(BackgroundSubagentWork))]
[JsonSerializable(typeof(BackgroundWork))]
[JsonSerializable(typeof(BackgroundWorkKind))]
[JsonSerializable(typeof(CanvasesCapability))]
[JsonSerializable(typeof(CanvasExtensionDeclaration))]
[JsonSerializable(typeof(CanvasReference))]
[JsonSerializable(typeof(CanvasRegistry))]
[JsonSerializable(typeof(CanvasState))]
[JsonSerializable(typeof(CanvasStateChangedAction))]
[JsonSerializable(typeof(CanvasTypeDeclaration))]
[JsonSerializable(typeof(Changeset))]
[JsonSerializable(typeof(ChangesetCapabilities))]
[JsonSerializable(typeof(ChangesetClearedAction))]
Expand Down Expand Up @@ -120,6 +124,8 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(ChatBackgroundWorkRemovedAction))]
[JsonSerializable(typeof(ChatBackgroundWorkSetAction))]
[JsonSerializable(typeof(ChatCanvasesChangedAction))]
[JsonSerializable(typeof(ChatCanvasRegistryChangedAction))]
[JsonSerializable(typeof(ChatCanvasRegistryRequestedAction))]
[JsonSerializable(typeof(ChatChangesetsChangedAction))]
[JsonSerializable(typeof(ChatConfigChangedAction))]
[JsonSerializable(typeof(ChatConfigState))]
Expand Down Expand Up @@ -228,6 +234,7 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(DirectoryCustomization))]
[JsonSerializable(typeof(DirectoryEntry))]
[JsonSerializable(typeof(DispatchActionParams))]
[JsonSerializable(typeof(DisposeCanvasParams))]
[JsonSerializable(typeof(DisposeChatParams))]
[JsonSerializable(typeof(DisposeSessionParams))]
[JsonSerializable(typeof(DisposeTerminalParams))]
Expand Down Expand Up @@ -290,6 +297,7 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(MultipleChatProvidersCapability))]
[JsonSerializable(typeof(MultipleChatsCapability))]
[JsonSerializable(typeof(MultipleWorkingDirectoriesCapability))]
[JsonSerializable(typeof(OpenCanvasParams))]
[JsonSerializable(typeof(OtlpExportLogsParams))]
[JsonSerializable(typeof(OtlpExportMetricsParams))]
[JsonSerializable(typeof(OtlpExportTracesParams))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,22 @@ public sealed record AgentInfo
/// per-capability options.</summary>
public sealed record AgentCapabilities
{
/// <summary>Experimental Canvas API.
///
/// The agent can materialize an exact chat's
/// {@link ChatState.canvasRegistry | Canvas registry} through
/// `chat/canvasRegistryRequested` and open a declared type with `openCanvas`.
/// Live instances are published through the chat's canvas references and
/// their `ahp-canvas:` state channels.
///
/// Clients MUST treat absence as unsupported and SHOULD tolerate additive
/// registry metadata while the Canvas channel remains experimental. This
/// capability does not gate server-published Canvas references.
///
/// Stability: 1.0 - Early development.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CanvasesCapability? Canvases { get; init; }

/// <summary>The agent can host more than one concurrent chat per session. When absent,
/// clients MUST NOT call `createChat` to open chats beyond the default one the
/// session starts with. An empty object `{}` advertises multi-chat without
Expand All @@ -2074,6 +2090,14 @@ public sealed record AgentCapabilities
public MultipleWorkingDirectoriesCapability? MultipleWorkingDirectories { get; init; }
}

/// <summary>Presence capability for the experimental Canvas registry and lifecycle
/// commands.
///
/// Stability: 1.0 - Early development.</summary>
public sealed record CanvasesCapability
{
}

/// <summary>Options for the {@link AgentCapabilities.multipleChats} capability.</summary>
public sealed record MultipleChatsCapability
{
Expand Down Expand Up @@ -2545,6 +2569,19 @@ public sealed class ChatState
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<BackgroundWork>? BackgroundWork { get; set; }

/// <summary>Canvas types currently registered for this exact chat.
///
/// Absence means the registry has not been materialized or is not currently
/// available. Clients that need it MAY dispatch
/// `chat/canvasRegistryRequested`; the host publishes the authoritative
/// registry through `chat/canvasRegistryChanged`. An empty
/// {@link CanvasRegistry.extensions} array is a loaded registry with no
/// available canvas types.
///
/// Stability: 1.0 - Early development.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CanvasRegistry? CanvasRegistry { get; set; }

/// <summary>Live canvases currently exposed by this chat.
///
/// Entries intentionally contain only subscribable channel references.
Expand Down Expand Up @@ -2606,6 +2643,51 @@ public sealed class ChatState
public Dictionary<string, JsonElement>? Meta { get; set; }
}

/// <summary>One namespace that declares canvas types for a chat.
///
/// `extensionId` is opaque. Clients MUST NOT infer management behavior by
/// parsing it.</summary>
public sealed record CanvasExtensionDeclaration
{
/// <summary>Stable opaque declaration namespace.</summary>
public required string ExtensionId { get; init; }

/// <summary>Human-readable namespace title, when available.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ExtensionName { get; init; }

/// <summary>Canvas types declared by this extension/provider. `canvasId` MUST be
/// unique within this array.</summary>
public required List<CanvasTypeDeclaration> Canvases { get; init; }
}

/// <summary>One canvas type currently available to open.
///
/// This experimental object may gain optional discovery metadata. Clients
/// SHOULD ignore fields they do not understand.</summary>
public sealed record CanvasTypeDeclaration
{
/// <summary>Provider-local canvas type identifier.</summary>
public required string CanvasId { get; init; }

/// <summary>Human-readable canvas type title.</summary>
public required string Title { get; init; }

/// <summary>Description of what the canvas type does.</summary>
public required string Description { get; init; }

/// <summary>Whether opening this canvas requires an input flow.</summary>
public bool RequiresInput { get; init; }
}

/// <summary>Canvas types currently registered for one chat.</summary>
public sealed record CanvasRegistry
{
/// <summary>Declaring namespaces. `extensionId` MUST be unique within this
/// array. An empty array is an authoritative loaded registry with no types.</summary>
public required List<CanvasExtensionDeclaration> Extensions { get; init; }
}

/// <summary>Stable reference to a subscribable canvas channel.
///
/// Chat state intentionally carries only this reference so the experimental
Expand All @@ -2624,13 +2706,17 @@ public sealed record CanvasReference
/// redundantly carry its channel URI.</summary>
public sealed class CanvasState
{
/// <summary>Stable caller-supplied instance identifier.</summary>
/// <summary>Provider-facing instance identifier.
///
/// AHP clients address this instance by the subscribed `ahp-canvas:` channel
/// URI. They MUST NOT treat this provider identifier as a second resource
/// identity.</summary>
public required string InstanceId { get; set; }

/// <summary>Owning extension/provider identifier.</summary>
/// <summary>Owning declaration namespace.</summary>
public required string ExtensionId { get; set; }

/// <summary>Owning extension display name, when available.</summary>
/// <summary>Owning namespace title, when available.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ExtensionName { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public static bool TryGetActionType(object action, out ActionType actionType)
case ChatCanvasesChangedAction value:
actionType = value.Type;
return true;
case ChatCanvasRegistryChangedAction value:
actionType = value.Type;
return true;
case ChatCanvasRegistryRequestedAction value:
actionType = value.Type;
return true;
case ChatChangesetsChangedAction value:
actionType = value.Type;
return true;
Expand Down
6 changes: 6 additions & 0 deletions clients/dotnet/src/AgentHostProtocol/Reducers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,11 @@ public static ReduceOutcome ApplyToChat(ChatState state, StateAction action)
case ChatCanvasesChangedAction a:
state.Canvases = CopyList(a.Canvases);
return ReduceOutcome.Applied;
case ChatCanvasRegistryRequestedAction:
return ReduceOutcome.NoOp;
case ChatCanvasRegistryChangedAction a:
state.CanvasRegistry = a.CanvasRegistry;
return ReduceOutcome.Applied;
case ChatWorkingDirectorySetAction a:
{
// Membership keyed by the directory URI, over this chat's subset of
Expand Down Expand Up @@ -2640,6 +2645,7 @@ public static ReduceOutcome ApplyToAutomationRun(
"chat/isReadChanged",
"chat/isArchivedChanged",
"chat/configChanged",
"chat/canvasRegistryRequested",
"chat/inputAnswerChanged",
"chat/inputCompleted",
"chat/truncated",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type,
return Wrap(Ser.Deserialize<ResourceReadParams>(inputJson));
case "ResourceReadResult":
return Wrap(Ser.Deserialize<ResourceReadResult>(inputJson));
case "OpenCanvasParams":
return Wrap(Ser.Deserialize<OpenCanvasParams>(inputJson));
case "DisposeCanvasParams":
return Wrap(Ser.Deserialize<DisposeCanvasParams>(inputJson));
default:
throw new Xunit.Sdk.XunitException(
$"round-trip fixture: unknown wire type \"{type}\". "
Expand Down
5 changes: 5 additions & 0 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,11 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R
state.Canvases = append([]ahptypes.CanvasReference(nil), a.Canvases...)
}
return ReduceOutcomeApplied
case *ahptypes.ChatCanvasRegistryRequestedAction:
return ReduceOutcomeNoOp
case *ahptypes.ChatCanvasRegistryChangedAction:
state.CanvasRegistry = a.CanvasRegistry
return ReduceOutcomeApplied
case *ahptypes.ChatWorkingDirectorySetAction:
for _, d := range state.WorkingDirectories {
if d == a.Directory {
Expand Down
Loading
Loading