Skip to content
Merged
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
31 changes: 28 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,38 @@ changes accumulate. Track in-flight protocol changes via PRs touching
`NOTIFICATION_INTRODUCED_IN` maps in
[`types/version/registry.ts`](types/version/registry.ts).

## [0.10.0] — Unreleased
## [1.1.0] — Unreleased

Spec version: `0.10.0`
Spec version: `1.1.0`

## [1.0.0] — 2026-10-02

Spec version: `1.0.0`

### Added

- Chat-owned canvas references and experimental per-canvas channels for live presentation state and source URLs.
- `AuthenticateParams.expiresIn` carries an OAuth access token's remaining lifetime in seconds.
- `SessionSummary.chats`, `SessionChatSummary.interactivity`, and `SessionSummary.defaultChat` expose lightweight chat navigation data without requiring a session subscription.
- `chat/isArchivedChanged` action for archiving a chat independently of its session.
- `ChatState.changesets` and `chat/changesetsChanged` expose each chat's changeset catalogue through the subscribed chat channel.
- `AutomationDefinition.disableConditions` supports `afterRuns` (`max`) and `afterDate` (`date`) rules combined with logical OR, with at most one condition per kind. Host-owned `AutomationEntry.runCount` tracks the current scheduled-run allowance. Edit conditions through `automation/updateRequested`: omission leaves them unchanged, an array replaces them, and `[]` removes all conditions without re-enabling the automation.
- `ChangesetStatus.Recomputing` distinguishes refreshes of a completed result from initial changeset computation.
- `ConfigPropertySchema.minItems` and `ConfigPropertySchema.maxItems` express array cardinality for `type: "array"` config properties. (#432)
- `McpServerStartingState.blocking` flag indicating that message processing may be held on the server, and the client-dispatchable `session/mcpServerBackgroundRequested` action asking the host to background such a startup.
- `AutomationSessionTemplate.customizations` lets automations carry client plugins (skills, agents, prompts, rules) that the host captures when the definition is saved, with the host-owned copies reported in `AutomationEntry.customizations` and support advertised by `AutomationCapabilities.customizations`.
- Hosts can advertise chat-owned canvas channel references and synchronize live presentation metadata and source URLs on experimental per-canvas channels.
- Atomic stable-URI `moveChat` transfer and same-session ordering for host-authorized chats, with per-chat `movable` discovery and durable authoritative catalogs.
- Expose chat-owned background work (background shells and subagents) in chat state, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime.
- `ChatSummary.changes` and `ChatState.changes` provide aggregate file-change counts without requiring a changeset subscription.
- `SessionChatSummary.status` exposes per-chat archived state via `SessionStatus.IsArchived` in the lightweight chat catalog without requiring a session subscription.
- `chat/isReadChanged` action and optional `SessionChatSummary.status` projection for independently tracking the status of any known chat, including the default chat; the status bitset replaces the unreleased `isRead` and `archived` catalog fields.
- `SessionChatSummary.changes` exposes per-chat change counts in the lightweight chat catalog without requiring a session subscription.

### Changed

- Historical `ToolResultTerminalContent.resource` subscriptions return lazily reconstructed exited terminal state with retained output.
- `FileEdit` and preview `edits` now use named models in generated SDKs. This breaks affected SDK APIs but keeps JSON shapes unchanged.
- Advertise only the 1.0.0 and 0.9.0 protocol compatibility baselines independently of the development version. Host-side negotiation helpers select the highest client-offered version within either caret-compatible range and report malformed offers explicitly.

## [0.9.0] — 2026-08-28

Expand Down
485 changes: 29 additions & 456 deletions clients/dotnet/CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion clients/dotnet/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.0
1.0.0
11 changes: 3 additions & 8 deletions clients/dotnet/release-metadata.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
{
"client": "dotnet",
"packageVersion": "0.9.0",
"packageVersion": "1.0.0",
"supportedProtocolVersions": [
"0.10.0",
"0.9.0",
"0.8.0",
"0.7.0",
"0.6.0",
"0.5.2",
"0.5.1"
"1.0.0",
"0.9.0"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ public sealed record InitializeParams
/// preferred to least preferred. Each entry is a [SemVer](https://semver.org)
/// `MAJOR.MINOR.PATCH` string (e.g. `"0.1.0"`).
///
/// The server selects one entry and returns it as `InitializeResult.protocolVersion`.
/// The server selects the highest offered SemVer within its supported
/// caret-compatible ranges and returns that exact entry as
/// `InitializeResult.protocolVersion`.
/// If the server cannot speak any of the offered versions, it MUST return
/// error code `-32005` (`UnsupportedProtocolVersion`) with required
/// `UnsupportedProtocolVersionErrorData` containing `supportedVersions`.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,71 @@ public static class ProtocolVersion
/// The current protocol version (SemVer MAJOR.MINOR.PATCH) this
/// generated source speaks.
/// </summary>
public const string Current = "0.10.0";
public const string Current = "1.1.0";

private static readonly string[] s_supported =
{
"0.10.0",
"1.0.0",
"0.9.0",
"0.8.0",
"0.7.0",
"0.6.0",
"0.5.2",
"0.5.1",
};

/// <summary>
/// Every protocol version this client is willing to negotiate, ordered
/// most-preferred-first. The first entry always equals <see cref="Current"/>.
/// most-preferred-first, independently of the development <see cref="Current"/>.
/// A fresh copy is returned on every call so callers may mutate it freely.
/// </summary>
public static IReadOnlyList<string> Supported => (string[])s_supported.Clone();

/// <summary>
/// Selects the highest offered version in a supported caret range.
/// Null means the host must send UnsupportedProtocolVersion and close.
/// Malformed versions throw ArgumentException.
/// </summary>
public static string? Negotiate(IEnumerable<string> offered)
{
Guard.ThrowIfNull(offered, nameof(offered));
static long[] Parse(string version)
{
if (version is null || !System.Text.RegularExpressions.Regex.IsMatch(
version, @"\A(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\z"))
throw new ArgumentException($"Invalid protocol version: {version}", nameof(version));
var parts = version.Split('.');
var values = new long[3];
for (var i = 0; i < 3; i++)
{
if (!long.TryParse(parts[i], System.Globalization.NumberStyles.None,
System.Globalization.CultureInfo.InvariantCulture, out values[i]))
throw new ArgumentException($"Invalid protocol version: {version}", nameof(version));
}
return values;
}
static int Compare(long[] a, long[] b)
{
for (var i = 0; i < 3; i++)
{
var comparison = a[i].CompareTo(b[i]);
if (comparison != 0) return comparison;
}
return 0;
}
var baselines = new List<long[]>();
foreach (var baseline in s_supported) baselines.Add(Parse(baseline));
string? selected = null;
long[]? previous = null;
foreach (var version in offered)
{
var parts = Parse(version);
if (baselines.Exists(b => parts[0] == b[0] && (parts[0] > 0 || parts[1] == b[1])
&& (parts[0] > 0 || parts[1] > 0 || parts[2] == b[2]) && Compare(parts, b) >= 0)
&& (previous is null || Compare(parts, previous) > 0))
{
selected = version;
previous = parts;
}
}
return selected;
}

/// <summary>The well-known channel URI for the root channel.</summary>
public const string RootResourceUri = "ahp-root://";
}
4 changes: 2 additions & 2 deletions clients/dotnet/tests/AgentHostProtocol.AotSmoke/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ static async Task RunTransportScenarioAsync(SystemTextJsonAhpSerializer serializ
"initialize",
new InitializeResult
{
ProtocolVersion = ProtocolVersion.Current,
ProtocolVersion = ProtocolVersion.Supported[0],
ServerSeq = 4,
Snapshots = new List<Snapshot>(),
},
cancellationToken);
InitializeResult initializeResult = await client.InitializeAsync("native-aot-client", cancellationToken: cancellationToken);
JsonRpcRequest initializeRequest = await initializeResponse;
Ensure(initializeResult.ProtocolVersion == ProtocolVersion.Current, "Initialize result failed.");
Ensure(initializeResult.ProtocolVersion == ProtocolVersion.Supported[0], "Initialize result failed.");
Ensure(
initializeRequest.Params?.GetProperty("clientId").GetString() == "native-aot-client",
"Initialize params failed.");
Expand Down
13 changes: 8 additions & 5 deletions clients/dotnet/tests/AgentHostProtocol.Tests/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ public static async Task HandleOneInitialize(MemTransport serverSide, Cancellati
Assert.NotNull(msg.Request);
Assert.Equal("initialize", msg.Request!.Method);

var result = new InitializeResult { ProtocolVersion = ProtocolVersion.Current, Snapshots = new() };
var offered = Ser.Deserialize<InitializeParams>(msg.Request.Params!.Value.GetRawText()).ProtocolVersions;
var selected = ProtocolVersion.Negotiate(offered)
?? throw new InvalidOperationException("Fake server received no compatible protocol version");
var result = new InitializeResult { ProtocolVersion = selected, Snapshots = new() };
var response = new JsonRpcMessage
{
SuccessResponse = new JsonRpcSuccessResponse
Expand Down Expand Up @@ -123,7 +126,7 @@ public async Task RequestRoundTrip_InitializeReturnsProtocolVersion()
await using var client = AhpClient.Connect(clientSide);
var result = await client.InitializeAsync("test-client", cancellationToken: cts.Token);

Assert.Equal(ProtocolVersion.Current, result.ProtocolVersion);
Assert.Equal(ProtocolVersion.Supported[0], result.ProtocolVersion);
await serverTask;
}

Expand Down Expand Up @@ -365,7 +368,7 @@ public async Task Request_HappyPath_StillResolves()
await using var client = AhpClient.Connect(clientSide);
var result = await client.InitializeAsync("test-client", cancellationToken: cts.Token);

Assert.Equal(ProtocolVersion.Current, result.ProtocolVersion);
Assert.Equal(ProtocolVersion.Supported[0], result.ProtocolVersion);
// The resolved request left no pending entry behind.
Assert.Equal(0, client.PendingRequestCount);
await serverTask;
Expand Down Expand Up @@ -548,7 +551,7 @@ public async Task Initialize_SnapshotDeliveredInResult()
// Server replies to `initialize` with a result carrying one snapshot.
var initResult = new InitializeResult
{
ProtocolVersion = ProtocolVersion.Current,
ProtocolVersion = ProtocolVersion.Supported[0],
ServerSeq = 7,
Snapshots = new System.Collections.Generic.List<Snapshot>
{
Expand All @@ -572,7 +575,7 @@ public async Task Initialize_SnapshotDeliveredInResult()
initialSubscriptions: new[] { "ahp-session:/s1" },
cancellationToken: cts.Token);

Assert.Equal(ProtocolVersion.Current, result.ProtocolVersion);
Assert.Equal(ProtocolVersion.Supported[0], result.ProtocolVersion);
Assert.NotNull(result.Snapshots);
var snapshot = Assert.Single(result.Snapshots);
Assert.Equal("ahp-session:/s1", snapshot.Resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ private static async Task RespondInitializeLoopAsync(MemTransport serverSide, Ca
if (msg.Request?.Method == "initialize")
await FakeHost.RespondResultAsync(
serverSide, msg.Request.Id,
new InitializeResult { ProtocolVersion = ProtocolVersion.Current, Snapshots = new() },
new InitializeResult { ProtocolVersion = ProtocolVersion.Supported[0], Snapshots = new() },
ct).ConfigureAwait(false);
}
}
Expand Down
4 changes: 2 additions & 2 deletions clients/dotnet/tests/AgentHostProtocol.Tests/HostsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public sealed class HostsTests
private static Task RunFakeServerAsync(MemTransport serverSide, CancellationToken ct) =>
FakeHost.New()
.OnInitialize((req, side, c) => FakeHost.RespondResultAsync(
side, req.Id, new InitializeResult { ProtocolVersion = ProtocolVersion.Current, Snapshots = new() }, c))
side, req.Id, new InitializeResult { ProtocolVersion = ProtocolVersion.Supported[0], Snapshots = new() }, c))
.RunAsync(serverSide, ct);

// ── Single host handshake ─────────────────────────────────────────────
Expand All @@ -44,7 +44,7 @@ public async Task SingleHostHandshake_ConnectedWithProtocolVersion()
await using var disposeMulti = multi;

Assert.Equal(HostStateKind.Connected, handle.State.Kind);
Assert.Equal(ProtocolVersion.Current, handle.ProtocolVersion);
Assert.Equal(ProtocolVersion.Supported[0], handle.ProtocolVersion);
Assert.False(string.IsNullOrEmpty(handle.ClientId),
"ClientID should be auto-generated and non-empty");
_ = serverTask; // referenced to avoid unused-variable warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private static async Task RepeatActionAsync(

private static Task RespondInitializeAsync(MemTransport serverSide, ulong id, CancellationToken ct) =>
FakeHost.RespondResultAsync(
serverSide, id, new InitializeResult { ProtocolVersion = ProtocolVersion.Current, Snapshots = new() }, ct);
serverSide, id, new InitializeResult { ProtocolVersion = ProtocolVersion.Supported[0], Snapshots = new() }, ct);

private static Task SendActionAsync(
MemTransport serverSide, string channel, long serverSeq, CancellationToken ct) =>
Expand Down Expand Up @@ -174,7 +174,7 @@ public async Task MultiHost_AddHostSnapshotsConfigurationBeforeFirstAwait()
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var store = new BlockingClientIdStore();
var subscriptions = new List<string> { "ahp-test://original" };
var protocolVersions = new List<string> { ProtocolVersion.Current };
var protocolVersions = new List<string> { ProtocolVersion.Supported[0] };
var observedInitialize = new TaskCompletionSource<InitializeParams>(
TaskCreationOptions.RunContinuationsAsynchronously);
await using var multiHost = new MultiHostClient(store);
Expand Down Expand Up @@ -208,7 +208,7 @@ public async Task MultiHost_AddHostSnapshotsConfigurationBeforeFirstAwait()
await addTask;
var initialize = await observedInitialize.Task.WaitAsync(cts.Token);
Assert.Equal("ahp-test://original", Assert.Single(initialize.InitialSubscriptions!));
Assert.Equal(ProtocolVersion.Current, Assert.Single(initialize.ProtocolVersions));
Assert.Equal(ProtocolVersion.Supported[0], Assert.Single(initialize.ProtocolVersions));
}

// ── H: events tagged hostId ────────────────────────────────────────────
Expand Down Expand Up @@ -965,7 +965,7 @@ private static Task RespondInitializeWithRootAsync(
{
var result = new InitializeResult
{
ProtocolVersion = ProtocolVersion.Current,
ProtocolVersion = ProtocolVersion.Supported[0],
ServerSeq = 0,
Snapshots = new List<Snapshot>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ private static TransportMessage BuildActionNotification(string uri, long serverS
private static Task RespondInitializeAsync(MemTransport serverSide, ulong id, CancellationToken ct) =>
FakeHost.RespondResultAsync(
serverSide, id,
new InitializeResult { ProtocolVersion = ProtocolVersion.Current, Snapshots = new() }, ct);
new InitializeResult { ProtocolVersion = ProtocolVersion.Supported[0], Snapshots = new() }, ct);

private static async Task DriveOneInitializeAsync()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void ProtocolVersionConstants()
string.IsNullOrWhiteSpace(ProtocolVersion.Current),
"ProtocolVersion.Current must be non-empty");
Assert.NotEmpty(ProtocolVersion.Supported);
Assert.Equal(ProtocolVersion.Current, ProtocolVersion.Supported[0]);
Assert.Equal(new[] { "1.0.0", "0.9.0" }, ProtocolVersion.Supported);
}

// ── Verifier ──────────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using Xunit;

namespace Microsoft.AgentHostProtocol.Tests;

public sealed class VersionNegotiationTests
{
[Fact]
public void SharedCorpus()
{
var root = new DirectoryInfo(AppContext.BaseDirectory);
while (!File.Exists(Path.Combine(root.FullName, "types", "test-cases", "version-negotiation.json")))
{
root = root.Parent ?? throw new DirectoryNotFoundException("Cannot locate version negotiation corpus");
}
using var document = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root.FullName, "types", "test-cases", "version-negotiation.json")));
Assert.NotEqual(0, document.RootElement.GetArrayLength());
foreach (var fixture in document.RootElement.EnumerateArray())
{
var offered = fixture.GetProperty("offered").EnumerateArray()
.Select(value => value.GetString() ?? throw new InvalidDataException("Null offered version")).ToArray();
if (fixture.TryGetProperty("invalid", out var invalid) && invalid.GetBoolean())
{
Assert.Throws<ArgumentException>(() => ProtocolVersion.Negotiate(offered));
}
else
{
Assert.Equal(fixture.GetProperty("expected").GetString(), ProtocolVersion.Negotiate(offered));
}
}
}
}
Loading
Loading