Skip to content
Draft
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
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,58 @@ jobs:
- name: Test Go module
run: go test ./...

dotnet:
runs-on: ubuntu-latest
defaults:
run:
working-directory: clients/dotnet
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

# The .NET 10 SDK builds the net8.0 target and understands the .slnx
# solution format; the 8.0.x entry provides the net8.0 runtime so the
# net8.0 tests run natively.
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x

- name: Install Node deps
working-directory: .
run: npm ci

# Verify the committed C# sources are in sync with the TypeScript
# protocol definitions. `git status --porcelain` (like the Kotlin / Go
# jobs) so a newly-emitted file also fails the check.
- name: Verify generated .NET is up to date
working-directory: .
run: |
npm run generate:dotnet
if [ -n "$(git status --porcelain -- clients/dotnet)" ]; then
echo "::error::Generated .NET sources are out of date. Run 'npm run generate:dotnet' and commit the result."
git status --porcelain -- clients/dotnet
git --no-pager diff -- clients/dotnet
exit 1
fi

- name: Restore .NET solution
run: dotnet restore

# Whitespace formatting gate — the C# analog of the Go job's gofmt check,
# governed by clients/dotnet/.editorconfig.
- name: Verify .NET formatting
run: dotnet format whitespace --verify-no-changes --no-restore

- name: Build .NET solution
run: dotnet build --no-restore --configuration Release

- name: Test .NET solution
run: dotnet test --no-build --configuration Release

8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

Cross-cutting rules for AI coding agents working in this repository. Per-client
codegen conventions are in `clients/kotlin/AGENTS.md`,
`clients/swift/AGENTS.md`, and `clients/go/AGENTS.md`. Editorial rules
`clients/swift/AGENTS.md`, `clients/go/AGENTS.md`, and
`clients/dotnet/AGENTS.md`. Editorial rules
for protocol types are in
`.github/instructions/general-instructions.instructions.md`. Release mechanics
are in [`RELEASING.md`](RELEASING.md).

## Updating CHANGELOGs

This repo ships six independently-versioned artifacts (the spec plus
the Rust / Kotlin / Swift / TypeScript / Go clients), each with its
This repo ships seven independently-versioned artifacts (the spec plus
the Rust / Kotlin / Swift / TypeScript / Go / .NET clients), each with its
own `CHANGELOG.md` in Keep-a-Changelog format. The publish workflows
refuse to release a tag whose matching `## [X.Y.Z]` heading is
missing, so every user-visible change should land its CHANGELOG bullet
Expand Down Expand Up @@ -50,6 +51,7 @@ Map source paths to changelogs:
| `clients/swift/**` (non-generated) | `clients/swift/CHANGELOG.md` only. |
| `clients/typescript/**` (non-generated) | `clients/typescript/CHANGELOG.md` only. |
| `clients/go/**` (non-generated) | `clients/go/CHANGELOG.md` only. |
| `clients/dotnet/**` (non-generated) | `clients/dotnet/CHANGELOG.md` only. |
| `schema/**` | Root `CHANGELOG.md` (the schema is a spec output). |
| `scripts/generate*.ts` that changes any client's generated output | Every affected client's `CHANGELOG.md`. |

Expand Down
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ against them.
| `clients/kotlin/` | Kotlin/JVM library (`com.microsoft.agenthostprotocol:agent-host-protocol`). |
| `clients/swift/` | Swift package (consumed by SwiftPM at the repo root). |
| `clients/typescript/` | npm package `@microsoft/agent-host-protocol`. |
| `clients/go/` | Go module (`ahptypes`, `ahp`, `ahpws`). |
| `clients/dotnet/` | .NET / NuGet packages (`Microsoft.AgentHostProtocol`, `.Abstractions`, `.WebSockets`). |
| `.github/workflows/` | CI and per-artifact publish pipelines. |

## Local dev loop
Expand All @@ -42,6 +44,8 @@ cd clients/typescript && npm ci && npm test && npm run build
cd clients/rust && cargo test --workspace
cd clients/kotlin && ./gradlew build
swift build && swift test # Swift uses the root Package.swift
cd clients/go && go test ./...
cd clients/dotnet && dotnet test
```

## Releases
Expand All @@ -53,7 +57,7 @@ see [`docs/specification/versioning.md`](docs/specification/versioning.md).

## Updating CHANGELOGs

This repo ships five independently-versioned artifacts (spec + four clients),
This repo ships seven independently-versioned artifacts (spec + six clients),
each with its own `CHANGELOG.md` in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
format. The publish workflows refuse to release a tag whose matching
`## [X.Y.Z]` heading is missing, so every user-visible change should land its
Expand Down Expand Up @@ -94,4 +98,5 @@ When iterating on the protocol surface in `types/`, see
for the project's editorial rules on type changes.

For language-specific code-gen conventions, see the `AGENTS.md` file in each
client directory (`clients/kotlin/AGENTS.md`, `clients/swift/AGENTS.md`).
client directory (`clients/go/AGENTS.md`, `clients/kotlin/AGENTS.md`,
`clients/swift/AGENTS.md`, `clients/dotnet/AGENTS.md`).
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ The Agent Host Protocol (AHP) defines how a portable, standalone sessions server
- **Kotlin** — Add `com.microsoft.agenthostprotocol:agent-host-protocol` from Maven Central to use from Android or any JVM project. See [`clients/kotlin/`](clients/kotlin/) for the source and [`CHANGELOG`](clients/kotlin/CHANGELOG.md). Released via `kotlin/vX.Y.Z` tags.
- **TypeScript** — Install `@microsoft/agent-host-protocol` to use the wire types, reducers, `AhpClient`, and the `WebSocketTransport`. See [`clients/typescript/`](clients/typescript/) and [`CHANGELOG`](clients/typescript/CHANGELOG.md). Released via `typescript/vX.Y.Z` tags; the Azure DevOps publish pipeline at [`clients/typescript/pipeline.yml`](clients/typescript/pipeline.yml) picks up the tag, validates it, and publishes to npm.
- **Go** — `go get github.com/microsoft/agent-host-protocol/clients/go` to use the `ahptypes` wire types, the `ahp` async client (client + pure reducers + pluggable `Transport`), and the `ahpws` WebSocket transport. See [`clients/go/`](clients/go/) and [`CHANGELOG`](clients/go/CHANGELOG.md). Released via `clients/go/vX.Y.Z` tags — the Go module proxy indexes the directory-prefixed tag directly from this repo, so there is no separate package registry.
- **.NET** — Install `Microsoft.AgentHostProtocol` (and `Microsoft.AgentHostProtocol.WebSockets` for a `ClientWebSocket` transport) to use the wire types, the pure reducers, the async `AhpClient`, and the `MultiHostClient`. The `Microsoft.AgentHostProtocol.Abstractions` package carries the wire types + transport/serializer interfaces alone. See [`clients/dotnet/`](clients/dotnet/) and [`CHANGELOG`](clients/dotnet/CHANGELOG.md). Released to NuGet.org via `dotnet/vX.Y.Z` tags.
- **[AHPX](https://github.com/TylerLeonhardt/ahpx)** — A command-line and Node.js client for connecting to AHP servers, managing sessions, and sending prompts.
- **[VS Code](https://github.com/microsoft/vscode)** — VS Code includes Agent Sessions client code for working with AHP hosts.

### Servers

- **[VS Code agent host](https://github.com/microsoft/vscode)** — The reference AHP server implementation. Start in [`src/vs/platform/agentHost/node/`](https://github.com/microsoft/vscode/tree/main/src/vs/platform/agentHost/node) when browsing the repository.

For consumers that need to talk to two or more hosts at once, the Rust SDK ships a `MultiHostClient` abstraction in [`ahp::hosts`](https://docs.rs/ahp/latest/ahp/hosts/), the Swift SDK ships `MultiHostClient` in `AgentHostProtocolClient`, and the Go SDK ships `MultiHostClient` in [`ahp/hosts`](clients/go/ahp/hosts/). Single-host consumers use the same API via `MultiHostClient::single` in Rust, `MultiHostClient.single(...)` in Swift, or `hosts.Single(...)` in Go. See [Connecting to Multiple Hosts](https://microsoft.github.io/agent-host-protocol/guide/clients-multi-host) for the design and surface.
For consumers that need to talk to two or more hosts at once, the Rust SDK ships a `MultiHostClient` abstraction in [`ahp::hosts`](https://docs.rs/ahp/latest/ahp/hosts/), the Swift SDK ships `MultiHostClient` in `AgentHostProtocolClient`, the Go SDK ships `MultiHostClient` in [`ahp/hosts`](clients/go/ahp/hosts/), and the .NET SDK ships `MultiHostClient` in `Microsoft.AgentHostProtocol.Hosts`. Single-host consumers use the same API via `MultiHostClient::single` in Rust, `MultiHostClient.single(...)` in Swift, `hosts.Single(...)` in Go, or `MultiHostClient.SingleAsync(...)` in .NET. See [Connecting to Multiple Hosts](https://microsoft.github.io/agent-host-protocol/guide/clients-multi-host) for the design and surface.

## Versioning and releases

Expand Down
21 changes: 21 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and a checked-in `clients/<lang>/release-metadata.json`.
| TypeScript | `typescript/vX.Y.Z` | `clients/typescript/pipeline.yml` (Azure DevOps) | npm (`@microsoft/agent-host-protocol`) via ESRP. |
| Swift | `vX.Y.Z` (bare) | `.github/workflows/publish-swift.yml` | SwiftPM resolves the tag directly. |
| Go | `clients/go/vX.Y.Z` | `.github/workflows/publish-go.yml` | Go module proxy resolves the tag directly. |
| .NET | `dotnet/vX.Y.Z` | maintainer-owned pipeline (see below) | NuGet.org (`Microsoft.AgentHostProtocol`, `.Abstractions`, `.WebSockets`). |

> **Why Swift gets the bare semver tag namespace:** SwiftPM only resolves
> packages by matching plain `X.Y.Z` / `vX.Y.Z` git tags at the manifest's
Expand Down Expand Up @@ -146,6 +147,26 @@ trigger started the run.
`go get github.com/microsoft/agent-host-protocol/clients/go@vX.Y.Z`;
no registry push happens.

### .NET (`dotnet/vX.Y.Z`)

1. Update `clients/dotnet/VERSION` to the new bare semver string (no
leading `v`).
2. Run `npm run generate:metadata` and commit the regenerated
`clients/dotnet/release-metadata.json`.
3. Rotate `clients/dotnet/CHANGELOG.md`.
4. Merge to `main`.
5. Tag: `git tag dotnet/v0.X.Y && git push origin dotnet/v0.X.Y`.
6. Publish the libraries (`Microsoft.AgentHostProtocol`,
`Microsoft.AgentHostProtocol.Abstractions`,
`Microsoft.AgentHostProtocol.WebSockets`) to NuGet.org. This client does
not ship its own publish automation — the maintainers wire the
`dotnet pack` + `dotnet nuget push` step into their own release pipeline,
the same way the Kotlin and TypeScript packages publish through the signed
Azure DevOps / ESRP pipelines rather than a GitHub Actions registry push.
The per-PR CI job already builds, tests, and runs the test-parity gate for
the solution; `npm run verify:changelog` guards the
`clients/dotnet/VERSION` ↔ `CHANGELOG.md` heading match.

### Spec (`spec/vX.Y.Z`)

1. Bump `PROTOCOL_VERSION` in `types/version/registry.ts` (and, if the
Expand Down
12 changes: 12 additions & 0 deletions clients/dotnet/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Formatting conventions for the .NET client. Gated in CI by
# `dotnet format whitespace --verify-no-changes` (the C# analog of the Go
# lane's `gofmt -l` check), so whitespace cannot silently drift.
root = true

[*.cs]
indent_style = space
indent_size = 4
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
2 changes: 2 additions & 0 deletions clients/dotnet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
obj/
123 changes: 123 additions & 0 deletions clients/dotnet/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Agent Guide — .NET client

Conventions for AI coding agents working on the .NET client. Cross-cutting
repo rules are in the root [`AGENTS.md`](../../AGENTS.md); release mechanics
are in [`RELEASING.md`](../../RELEASING.md).

## Layout

| Path | Contents |
| --- | --- |
| `src/AgentHostProtocol.Abstractions/Generated/*.generated.cs` | **Generated** wire types. Do not edit. |
| `src/AgentHostProtocol.Abstractions/Json/`, `Transport/` | Hand-written serialization support (`AhpUnion`, `UnionConverter`, `WireEnumConverter`, `StringOrMarkdown`) and the `ITransport` / `IAhpSerializer` seams. |
| `src/AgentHostProtocol/` | `AhpClient`, the reducers, the default `SystemTextJsonAhpSerializer`, subscriptions, and the `Hosts/` multi-host runtime. |
| `src/AgentHostProtocol.WebSockets/` | `ClientWebSocket`-based transport. |
| `tests/AgentHostProtocol.Tests/` | xUnit tests, including the shared reducer-fixture conformance suite. |
| `examples/` | Runnable console samples. |

## Code generation

Generated files are produced by `scripts/generate-csharp.ts` (run from the repo
root via `npm run generate:dotnet`) from the TypeScript definitions in
`types/`. The generator is modeled on `scripts/generate-go.ts` and shares its
curated struct / enum / union lists — they are protocol-driven, not
language-specific. After changing anything under `types/`, regenerate and
commit; CI fails on any diff between the committed sources and a fresh run.

## Type mapping (TS → C#)

- `number` → `long` (or `double` when the property carries `@format float`).
- `unknown` / `object` → `System.Text.Json.JsonElement`;
`Record<string, unknown>` → `Dictionary<string, JsonElement>`.
- Optional (`?` / `| undefined` / `| null`) fields → nullable + `[JsonIgnore(
Condition = JsonIgnoreCondition.WhenWritingNull)]`. Required fields serialize
their value (a required reference left null serializes as `null`, mirroring
Go's `nil`-slice semantics).
- String enums → C# `enum` with `[WireValue("…")]` per member, (de)serialized
by `WireEnumConverter<T>`. Bitset enums → `[Flags] enum : uint`, serialized
as their numeric value so unknown future bits round-trip.
- Discriminated unions → a sealed wrapper deriving from `AhpUnion` (carrying
`object? Value`) plus a generated `UnionConverter<T>`. Unknown discriminator
values are preserved verbatim as a raw `JsonElement`.

## Reducers

The reducers are a faithful port of the Go client's `reducers.go` and mirror
the canonical TypeScript reducers. They mutate state in place. The shared
fixtures under `types/test-cases/reducers/*.json` are the cross-language parity
gate — run them with `dotnet test`. The `resourceWatch` reducer is an
intentional stub (parity with the Rust and Go clients).

## Testing

Run by `dotnet test` (against `net8.0`), all green (0 skipped):

1. **Shared reducer conformance** — `FixtureDrivenReducerTests` replays the 169
cross-language reducer fixtures (`types/test-cases/reducers/*.json`). The
whole set counts as a single `[Theory]`.
2. **Shared wire round-trip corpus** — `TypesRoundTripFixtures` data-drives the
language-agnostic round-trip corpus under `types/test-cases/round-trips/*.json`
through the REAL serializer, asserting decode → re-encode is a byte-exact
fixed point. A `[Theory]` (`CorpusFixture`) iterates every fixture in the dir.
3. **Native unit tests** — `ClientTests` (full `AhpClient` over an in-memory
`MemTransport`, the port of Go's `client_test.go`), `HostsTests`,
`MultiHostClientTests`, `MultiHostStateMirrorTests`, `NativeReducerTests`,
`ReconnectPolicyTests`, `ClientIdStoreTests`,
`FileClientIdStoreTests`, `TransportTests`, `WebSocketTransportTests`. The
multi-host / host / client fake servers share one declarative loop helper,
`FakeHost`.
4. **Cross-implementation convergence** — `CrossImplementationConvergenceTests`
replays a session trace captured from an INDEPENDENT host (a separate
WebSocket host on the canonical TS `sessionReducer`) and asserts byte-identical
convergence (`serverSeq` + host-authoritative `modifiedAt`).

Beyond CI, the **full `AhpClient` has been validated LIVE over a real WebSocket**
against a spec-faithful AHP host built on the canonical `sessionReducer`: the
real `initialize` request/response handshake, the snapshot in `InitializeResult`,
and the live `action` notification stream all converge with the host. (No
client in any language ships a real-socket integration test — they are all
mock-transport-based; this validation is run out-of-band rather than committed,
since it needs a Node host + the published package.)

Cross-language parity is verified by the shared fixture corpora the suite
replays — the 169 reducer fixtures (`types/test-cases/reducers/*.json`) and the
round-trip corpus (`types/test-cases/round-trips/*.json`), both of which every
client runs. (A .NET-only grep-based test-count gate used to live here; it was
retired in favor of relying on the shared corpora, which actually exercise the
behavior rather than counting method names.)

## Architecture decisions

- [`docs/decisions/sync.md`](docs/decisions/sync.md)
— the full menu of .NET synchronization primitives, the distinct concurrency
use cases in the client, which primitive each gets (`ConcurrentDictionary`
for the collections, `lock` for the `HostEntry` field-bundle, `SemaphoreSlim`
only for the WebSocket send path, `Channels`/`Interlocked`/`volatile`
elsewhere), and why the client targets `net8.0` only.
- [`docs/decisions/serialization.md`](docs/decisions/serialization.md)
— System.Text.Json (default, in-box, fastest) behind the `IAhpSerializer`
seam, versus Newtonsoft / lazy-DOM / validating options, across speed,
memory, lazy-vs-eager, validation, dependencies, and AOT.
- [`docs/decisions/reconnect.md`](docs/decisions/reconnect.md)
— hand-rolled exponential backoff (with opt-in jitter) versus
Polly / `Microsoft.Extensions.Resilience`, and why the core stays
dependency-free.

These decision records live under `docs/decisions/` and are repo-only — they are not packed into any NuGet
package (only `README.md` is).

## Releasing

Sub-package releases publish the `Microsoft.AgentHostProtocol*` packages to
NuGet.org. This client does not ship its own publish automation; the
maintainers wire `dotnet pack` + `dotnet nuget push` into their own release
pipeline (e.g. the signed Azure DevOps / ESRP pipeline used for the Kotlin and
TypeScript packages). The `clients/dotnet/VERSION` ↔ `CHANGELOG.md` heading
match is enforced for every PR by `npm run verify:changelog`.

## Out of scope

JSON-Schema validation (a `Microsoft.AgentHostProtocol.Validation` decorator
over `IAhpSerializer`) and DI/extension helpers
(`Microsoft.AgentHostProtocol.Extensions`) are planned follow-ups, not part of
this client yet.
14 changes: 14 additions & 0 deletions clients/dotnet/AgentHostProtocol.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Solution>
<Folder Name="/examples/">
<Project Path="examples/ConnectWs/ConnectWs.csproj" />
<Project Path="examples/ReducersDemo/ReducersDemo.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/AgentHostProtocol.Abstractions/AgentHostProtocol.Abstractions.csproj" />
<Project Path="src/AgentHostProtocol.WebSockets/AgentHostProtocol.WebSockets.csproj" />
<Project Path="src/AgentHostProtocol/AgentHostProtocol.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/AgentHostProtocol.Tests/AgentHostProtocol.Tests.csproj" />
</Folder>
</Solution>
Loading