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
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,61 @@ 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

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

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

# Blocking test-parity gate: fails while any test in the parity manifest
# (clients/dotnet/tests/parity-manifest.txt) is missing, enumerating exactly
# which methods to add. Green only at full Swift/TS parity. See the parity
# contract in clients/dotnet/AGENTS.md ("Test-parity gate"). Runs last so the
# build/test signal above is still reported while parity is in progress.
- name: Enforce .NET test parity (blocking; enumerates missing tests)
run: bash clients/dotnet/scripts/check-test-parity.sh

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
4 changes: 4 additions & 0 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 Down
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
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/
155 changes: 155 additions & 0 deletions clients/dotnet/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 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 both target frameworks, `net8.0` and `net9.0`).
The suite is **326 tests, all green on both TFMs** (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]` iterates every fixture; named `[Fact]` wrappers in
`TypesRoundTripTests` carry the cross-language matrix method names.
3. **Native unit tests** — `ClientTests` (full `AhpClient` over an in-memory
`MemTransport`, the port of Go's `client_test.go`), `HostsTests`,
`MultiHostClientTests`, `MultiHostStateMirrorTests`, `NativeReducerTests`,
`TypesRoundTripTests`, `ReconnectPolicyTests`, `ClientIdStoreTests`,
`FileClientIdStoreTests`, `TransportTests`, `WebSocketTransportTests`.
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.)

### Test-parity gate

Two layers enforce **manifest parity** — the machine-checked cross-language
matrix subset. Both run [`scripts/check-test-parity.sh`](scripts/check-test-parity.sh) against
[`tests/parity-manifest.txt`](tests/parity-manifest.txt) — the expected parity
test methods in executable form — plus the count floor in
[`tests/MIN_TEST_COUNT`](tests/MIN_TEST_COUNT).

The manifest currently enumerates **75 method names** and all 75 are present
(75/75). Read that precisely: it is the cross-language matrix *subset* that has
been transcribed into the manifest, **not** a literal mirror of the entire Swift
suite. Some Swift behaviors — notably a number of §H `MultiHostClient`
edge-cases and several sub-cases — are not yet enumerated in the manifest, so
"75/75 manifest parity" is a green gate, not a claim of complete Swift parity.
When you add tests that close one of those gaps, add the method name to the
manifest (and `--bump` the floor) so the matrix subset grows with the suite.

- **CI (blocking):** `.github/workflows/ci.yml` runs the gate in COMPLETE mode —
it **fails the build while any manifest test is missing**, and the error
enumerates exactly which test methods to add (grouped by phase) and references
the parity manifest. Green only when every *manifest* method is present.
- **Local pre-push (ratchet):** the hook runs `--ratchet`, which blocks a push
only if the discrete `[Fact]`/`[Theory]` count drops below the floor (catches
deletions). It never blocks in-progress work, so the incremental commits that
climb toward parity push fine.

Commands:

- **Activate the local hook** (per-clone; git hooks are never shared — run once
from the repo root): `git config core.hooksPath scripts/git-hooks`
- **See what's still missing:** `clients/dotnet/scripts/check-test-parity.sh --list`
- **Raise the count floor after adding tests:**
`clients/dotnet/scripts/check-test-parity.sh --bump`

Neither layer runs `dotnet test`; test *correctness* is enforced by the
`dotnet test` step in CI. The 169 shared reducer fixtures count as one `[Theory]`,
so they do not inflate the floor.

## 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 libraries multi-target `net8.0;net9.0` to use
`System.Threading.Lock` on .NET 9.
- [`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