diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 45da8df7c..83ed8b177 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -306,8 +306,21 @@ jobs:
- name: Build .NET solution
run: dotnet build --no-restore --configuration Release
+ - name: Publish and run .NET Native AOT smoke test
+ run: |
+ dotnet publish tests/AgentHostProtocol.AotSmoke/AgentHostProtocol.AotSmoke.csproj \
+ --configuration Release \
+ --runtime linux-x64 \
+ --self-contained true \
+ --output artifacts/aot-smoke
+ ./artifacts/aot-smoke/AgentHostProtocol.AotSmoke
+
- name: Test .NET solution
- run: dotnet test --no-build --configuration Release
+ run: |
+ timeout --kill-after=30s 6m \
+ dotnet tests/AgentHostProtocol.Tests/bin/Release/net8.0/AgentHostProtocol.Tests.dll \
+ --timeout 5m \
+ --long-running 30
- name: Pack .NET solution
run: dotnet pack --no-build --configuration Release
diff --git a/clients/dotnet/AgentHostProtocol.slnx b/clients/dotnet/AgentHostProtocol.slnx
index 582f1210c..c9ea641c3 100644
--- a/clients/dotnet/AgentHostProtocol.slnx
+++ b/clients/dotnet/AgentHostProtocol.slnx
@@ -8,6 +8,7 @@
+
diff --git a/clients/dotnet/README.md b/clients/dotnet/README.md
index 29d8d2e33..c14ac6d4d 100644
--- a/clients/dotnet/README.md
+++ b/clients/dotnet/README.md
@@ -17,8 +17,8 @@ dotnet add package Microsoft.AgentHostProtocol
| `Microsoft.AgentHostProtocol.Abstractions` | Wire types + reducers' data contracts + the `ITransport` / `IAhpSerializer` interfaces. No I/O, no dependencies. Reference this alone to parse / construct AHP messages or implement a transport. |
| `Microsoft.AgentHostProtocol` | The async `AhpClient`, pure reducers, default System.Text.Json serializer, `ClientWebSocket` transport, and `MultiHostClient`. |
-(`Microsoft.AgentHostProtocol` references `.Abstractions` transitively, so most
-consumers add the two packages above.)
+`Microsoft.AgentHostProtocol` references `.Abstractions` transitively, so most
+consumers only add the main package.
## Quickstart
@@ -63,17 +63,29 @@ services.AddAgentHostProtocol(cfg => cfg.DefaultRequestTimeout = TimeSpan.FromSe
That registers `IAhpSerializer`, `IClientIdStore`, `MultiHostClient`, and an
`IAhpClientFactory` as singletons. Because a client needs a live transport,
-resolve the factory and call `ConnectAsync(transport)`:
+resolve the factory and call `Connect(transport)`:
```csharp
var factory = provider.GetRequiredService();
-await using var client = await factory.ConnectAsync(transport);
+await using var client = factory.Connect(transport);
```
The `MultiHostClient` singleton is disposed by the container on shutdown. The
`configureClient` options apply to the factory path; `MultiHostClient` hosts are
configured per host via `HostConfig.ClientConfig`.
+`ClientConfig.TimeProvider` controls request timeouts, keep-alive scheduling,
+reconnect backoff, and host timestamps. It defaults to `TimeProvider.System`;
+tests can supply a fake provider to advance these behaviors without wall-clock
+delays.
+
+`MultiHostClient.EventsForHost` and `MultiHostClient.Subscriptions` preserve
+resource continuity across reconnects. A replay is delivered as
+`SubscriptionEventAction` values; when the server must replace replay with fresh
+state, each returned resource is delivered as a `SubscriptionEventSnapshot`
+before the host reports `Connected`. Consumers should replace that resource's
+local reducer state with the snapshot before processing later actions.
+
## Observability
The client emits OpenTelemetry-native traces and metrics via `System.Diagnostics`
@@ -119,6 +131,29 @@ implementation can swap the engine or decorate it with JSON-Schema validation
(against the schemas the repository generates under `schema/`) without changing
the client or transport.
+### Native AOT and trimming
+
+The `net8.0` assets are trim- and Native AOT-compatible. The generator emits
+System.Text.Json metadata for the complete generated protocol model. The
+default serializer uses that metadata for JSON-RPC framing, discriminated
+unions, snapshots, and wire enums without enabling reflection serialization.
+`AhpJsonMetadata.Default` exposes the resolver without making the generated
+context's hundreds of implementation-detail properties public API.
+
+Custom values passed through `IAhpSerializer` need their own metadata when
+reflection is disabled. Add the application's context to the options before
+constructing the serializer; the serializer copies and freezes the options and
+adds the AHP context:
+
+```csharp
+var options = new JsonSerializerOptions();
+options.TypeInfoResolverChain.Add(MyApplicationJsonContext.Default);
+var serializer = new SystemTextJsonAhpSerializer(options);
+```
+
+CI publishes and runs `tests/AgentHostProtocol.AotSmoke` as a native executable
+with `JsonSerializerIsReflectionEnabledByDefault=false`.
+
## Releasing
1. Bump [`VERSION`](VERSION).
diff --git a/clients/dotnet/docs/decisions/serialization.md b/clients/dotnet/docs/decisions/serialization.md
index 8baa7414c..33eb2139d 100644
--- a/clients/dotnet/docs/decisions/serialization.md
+++ b/clients/dotnet/docs/decisions/serialization.md
@@ -43,8 +43,8 @@ the default is System.Text.Json and what the seam does and does not decouple.
| Option | Throughput | Allocations | Eager/Lazy | Deps | AOT | Notes |
| --- | --- | --- | --- | --- | --- | --- |
-| **System.Text.Json (POCO)** ✅ | Highest | Lowest (`Span`/UTF-8) | Eager | **In-box** (net8) | Source-gen capable | Strict by default; Microsoft's greenfield recommendation. |
-| System.Text.Json + source generation | Highest (+startup, +AOT) | Lowest | Eager | In-box | **Best** | An AOT/trimming enhancement for later — but **not** a drop-in `[JsonSerializable]` context: see "Deferred, on purpose" — the runtime-`Type`-keyed union converters do not compose with the source generator, so it requires reshaping the generated unions, not just adding a context. |
+| **System.Text.Json + source generation** ✅ | Highest (+startup, +AOT) | Lowest | Eager | **In-box** (net8) | **Best** | Generated metadata covers the complete wire model; custom converters resolve metadata before runtime-type dispatch. |
+| System.Text.Json (reflection contracts) | Highest | Lowest (`Span`/UTF-8) | Eager | In-box | Poor | Simpler initially, but unsuitable for trimming and Native AOT. |
| Newtonsoft.Json (Json.NET) | ~20–35% slower; ~3× more allocations on .NET 10 | High (reflection, no `Span`) | Eager or `JObject` (lazy, mutable) | **+dependency** | Reflection (AOT-hostile) | Lenient by default; ubiquitous, but a dependency and slower. |
| Lazy DOM — `JsonNode` / `JsonElement` (STJ) or `JObject` (Newtonsoft) | n/a (no bind) | Low for partial reads | **Lazy** | In-box (STJ) | ok | A *different consumption model*: expose untyped views instead of typed state. Reducers can't run on it without materializing. |
| Utf8Json / Jil / other high-perf | Very high | Very low | Eager | +dependency | varies | Effectively unmaintained; not worth the dependency/risk for a JSON wire protocol. |
@@ -72,14 +72,14 @@ Rationale, against the dimensions:
- **Dependencies:** STJ is **in the shared framework** for net8 — the
packages stay at **zero NuGet dependencies**, which is a hard goal for this
library.
-- **AOT / trimming:** STJ supports source generation as a path to
- Native-AOT/trimming friendliness later. Note this is **not** a free,
- drop-in step for this client: the discriminated unions dispatch on a
- runtime `Type`, which the source generator does not support, so the
- migration is a typed-variant rewrite of the generated unions (see
- "Deferred, on purpose"). Until then the shipping libraries declare the
- reflection unsafety via `[RequiresUnreferencedCode]`/`[RequiresDynamicCode]`
- on the serializer seam so trimmed/AOT consumers are warned at build time.
+- **AOT / trimming:** the C# generator emits metadata for every generated
+ protocol type and exposes it through the compact
+ `AhpJsonMetadata.Default` resolver API. Runtime-type union dispatch remains
+ compact, but resolves each known variant through
+ `JsonSerializerOptions.GetTypeInfo` and uses metadata-based serializer
+ overloads. The `net8.0` assets declare `IsTrimmable` and
+ `IsAotCompatible`; CI publishes and executes a reflection-disabled Native
+ AOT smoke application.
- **Strictness:** strict-by-default is correct for a wire protocol — a
malformed or unexpected frame should fail loudly, not be silently coerced.
- **Custom shapes:** the protocol's discriminated unions, `StringOrMarkdown`,
@@ -101,7 +101,7 @@ Rationale, against the dimensions:
mean re-emitting the types for that engine — tractable since they're
generated, but STJ stays the one default.
-### Deferred, on purpose
+### Boundaries and deferred work
- **Validation ("validated vs not"):** a future opt-in
`Microsoft.AgentHostProtocol.Validation` package will decorate
@@ -112,29 +112,28 @@ Rationale, against the dimensions:
without materializing typed state (a proxy/pass-through), that is a separate
read-only `JsonNode`/`JsonElement` surface — not a drop-in serializer swap,
because the reducers require typed state.
-- **Source generation:** add source-gen for AOT/trimming and a further perf
- bump when there is a concrete AOT consumer. This is **not** merely "add a
- `[JsonSerializable]` `JsonSerializerContext`." The union machinery is
- fundamentally reflection-polymorphic: `UnionConverter.Read` resolves the
- payload type at runtime from a `Dictionary` and calls
- `root.Deserialize(variantType, options)`, and `Write` serializes via
- `inner.GetType()`. The STJ source generator only emits metadata for
- compile-time-known closed types and does **not** support custom converters
- that dispatch on a runtime `Type`. A real source-gen migration therefore
- requires reshaping every discriminated union away from the `object?`-valued
- `AhpUnion` + runtime-`Type` dispatch toward a closed, statically-known variant
- representation (e.g. STJ's `[JsonPolymorphic]`/`[JsonDerivedType]`, or
- per-variant typed properties) — a redesign of the generated wire types plus
- the codegen, not a drop-in. In the meantime the libraries opt into the
- trim/AOT analyzers and annotate the reflection entry points with
- `[RequiresUnreferencedCode]`/`[RequiresDynamicCode]` so the limitation is
- declared at build time rather than discovered at runtime.
+- **Application-defined JSON types:** the AHP context intentionally contains
+ only protocol types. Consumers that pass their own types through
+ `IAhpSerializer` must add an `IJsonTypeInfoResolver` (normally their own
+ source-generated context) to the options supplied to
+ `SystemTextJsonAhpSerializer` when reflection serialization is disabled.
+ Reflection-enabled applications retain the generic serializer methods'
+ compatibility fallback for application-defined values.
+- **Generated union shape:** `AhpUnion.Value` still provides a compact common
+ representation and supports lossless unknown-value round trips. The
+ converter's runtime `Type` lookup is AOT-safe because every known variant has
+ generated metadata, but the wrapper can still represent an empty or
+ caller-supplied invalid state. Hardening that public shape would be a separate
+ compatibility decision.
## Consequences
- The default path is fast, allocation-light, and dependency-free.
+- Native AOT and trimmed applications use the default serializer without
+ reflection contracts.
- A different engine or a validating layer can be added behind `IAhpSerializer`
without touching the client or transport.
+- Custom application types require explicitly registered JSON metadata.
- Consumers who want JSON-Schema validation opt into a separate package; the
core never pays for it.
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/AgentHostProtocol.Abstractions.csproj b/clients/dotnet/src/AgentHostProtocol.Abstractions/AgentHostProtocol.Abstractions.csproj
index 27531a4d3..19dd159d7 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/AgentHostProtocol.Abstractions.csproj
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/AgentHostProtocol.Abstractions.csproj
@@ -13,14 +13,10 @@
I/O and no dependencies beyond the base class library; reference it to
parse, construct, or inspect AHP messages, or to implement a transport.
-
+
+ true
+ truetruetrue
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
new file mode 100644
index 000000000..6dd6f13a8
--- /dev/null
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -0,0 +1,460 @@
+//
+// Generated from types/*.ts — do not edit.
+//
+// Regenerate with: npm run generate:dotnet
+//
+#nullable enable
+
+namespace Microsoft.AgentHostProtocol;
+
+[JsonSerializable(typeof(ActionEnvelope))]
+[JsonSerializable(typeof(ActionOrigin))]
+[JsonSerializable(typeof(ActionType))]
+[JsonSerializable(typeof(ActiveTurn))]
+[JsonSerializable(typeof(AgentCapabilities))]
+[JsonSerializable(typeof(AgentCustomization))]
+[JsonSerializable(typeof(AgentInfo))]
+[JsonSerializable(typeof(AgentSelection))]
+[JsonSerializable(typeof(AhpMcpUiHostCapabilities))]
+[JsonSerializable(typeof(Annotation))]
+[JsonSerializable(typeof(AnnotationEntry))]
+[JsonSerializable(typeof(AnnotationOrigin))]
+[JsonSerializable(typeof(AnnotationsEntryRemovedAction))]
+[JsonSerializable(typeof(AnnotationsEntrySetAction))]
+[JsonSerializable(typeof(AnnotationsRemovedAction))]
+[JsonSerializable(typeof(AnnotationsSetAction))]
+[JsonSerializable(typeof(AnnotationsState))]
+[JsonSerializable(typeof(AnnotationsSummary))]
+[JsonSerializable(typeof(AnnotationsUpdatedAction))]
+[JsonSerializable(typeof(AuthenticateParams))]
+[JsonSerializable(typeof(AuthenticateResult))]
+[JsonSerializable(typeof(AuthRequiredErrorData))]
+[JsonSerializable(typeof(AuthRequiredParams))]
+[JsonSerializable(typeof(AuthRequiredReason))]
+[JsonSerializable(typeof(AutomationCancelledRunLifecycle))]
+[JsonSerializable(typeof(AutomationCapabilities))]
+[JsonSerializable(typeof(AutomationCatalogState))]
+[JsonSerializable(typeof(AutomationCompletedRunLifecycle))]
+[JsonSerializable(typeof(AutomationCreateCapability))]
+[JsonSerializable(typeof(AutomationCreateRequestedAction))]
+[JsonSerializable(typeof(AutomationDefinition))]
+[JsonSerializable(typeof(AutomationDefinitionPatch))]
+[JsonSerializable(typeof(AutomationEventTrigger))]
+[JsonSerializable(typeof(AutomationFailedRunLifecycle))]
+[JsonSerializable(typeof(AutomationManualRunOrigin))]
+[JsonSerializable(typeof(AutomationMisfirePolicy))]
+[JsonSerializable(typeof(AutomationOperation))]
+[JsonSerializable(typeof(AutomationPendingRunLifecycle))]
+[JsonSerializable(typeof(AutomationRemovedAction))]
+[JsonSerializable(typeof(AutomationRunCancellationCapability))]
+[JsonSerializable(typeof(AutomationRunCancelRequestedAction))]
+[JsonSerializable(typeof(AutomationRunLifecycle))]
+[JsonSerializable(typeof(AutomationRunLifecycleChangedAction))]
+[JsonSerializable(typeof(AutomationRunningRunLifecycle))]
+[JsonSerializable(typeof(AutomationRunOrigin))]
+[JsonSerializable(typeof(AutomationRunOriginKind))]
+[JsonSerializable(typeof(AutomationRunPrimarySessionChangedAction))]
+[JsonSerializable(typeof(AutomationRunSessionRemovedAction))]
+[JsonSerializable(typeof(AutomationRunSessionSetAction))]
+[JsonSerializable(typeof(AutomationRunState))]
+[JsonSerializable(typeof(AutomationRunStatus))]
+[JsonSerializable(typeof(AutomationRunSummary))]
+[JsonSerializable(typeof(AutomationSchedule))]
+[JsonSerializable(typeof(AutomationScheduleCapabilities))]
+[JsonSerializable(typeof(AutomationScheduleTrigger))]
+[JsonSerializable(typeof(AutomationSessionOrigin))]
+[JsonSerializable(typeof(AutomationSessionTemplate))]
+[JsonSerializable(typeof(AutomationSetAction))]
+[JsonSerializable(typeof(AutomationState))]
+[JsonSerializable(typeof(AutomationTrigger))]
+[JsonSerializable(typeof(AutomationTriggerDefinition))]
+[JsonSerializable(typeof(AutomationTriggeredRunOrigin))]
+[JsonSerializable(typeof(AutomationTriggerEventDefinition))]
+[JsonSerializable(typeof(AutomationTriggerKind))]
+[JsonSerializable(typeof(AutomationUpdateRequestedAction))]
+[JsonSerializable(typeof(Changeset))]
+[JsonSerializable(typeof(ChangesetCapabilities))]
+[JsonSerializable(typeof(ChangesetClearedAction))]
+[JsonSerializable(typeof(ChangesetContentChangedAction))]
+[JsonSerializable(typeof(ChangesetFile))]
+[JsonSerializable(typeof(ChangesetFileRemovedAction))]
+[JsonSerializable(typeof(ChangesetFileSetAction))]
+[JsonSerializable(typeof(ChangesetFilesReviewChangedAction))]
+[JsonSerializable(typeof(ChangesetOperation))]
+[JsonSerializable(typeof(ChangesetOperationFollowUp))]
+[JsonSerializable(typeof(ChangesetOperationRangeTarget))]
+[JsonSerializable(typeof(ChangesetOperationResourceTarget))]
+[JsonSerializable(typeof(ChangesetOperationsChangedAction))]
+[JsonSerializable(typeof(ChangesetOperationScope))]
+[JsonSerializable(typeof(ChangesetOperationStatus))]
+[JsonSerializable(typeof(ChangesetOperationStatusChangedAction))]
+[JsonSerializable(typeof(ChangesetOperationTarget))]
+[JsonSerializable(typeof(ChangesetState))]
+[JsonSerializable(typeof(ChangesetStatus))]
+[JsonSerializable(typeof(ChangesetStatusChangedAction))]
+[JsonSerializable(typeof(ChangesSummary))]
+[JsonSerializable(typeof(ChatActivityChangedAction))]
+[JsonSerializable(typeof(ChatDeltaAction))]
+[JsonSerializable(typeof(ChatDraftChangedAction))]
+[JsonSerializable(typeof(ChatErrorAction))]
+[JsonSerializable(typeof(ChatInputAnswer))]
+[JsonSerializable(typeof(ChatInputAnswerChangedAction))]
+[JsonSerializable(typeof(ChatInputAnswered))]
+[JsonSerializable(typeof(ChatInputAnswerState))]
+[JsonSerializable(typeof(ChatInputAnswerValue))]
+[JsonSerializable(typeof(ChatInputAnswerValueKind))]
+[JsonSerializable(typeof(ChatInputBooleanAnswerValue))]
+[JsonSerializable(typeof(ChatInputBooleanQuestion))]
+[JsonSerializable(typeof(ChatInputCompletedAction))]
+[JsonSerializable(typeof(ChatInputMultiSelectQuestion))]
+[JsonSerializable(typeof(ChatInputNumberAnswerValue))]
+[JsonSerializable(typeof(ChatInputNumberQuestion))]
+[JsonSerializable(typeof(ChatInputOption))]
+[JsonSerializable(typeof(ChatInputQuestion))]
+[JsonSerializable(typeof(ChatInputQuestionKind))]
+[JsonSerializable(typeof(ChatInputRequest))]
+[JsonSerializable(typeof(ChatInputRequestedAction))]
+[JsonSerializable(typeof(ChatInputResponseKind))]
+[JsonSerializable(typeof(ChatInputSelectedAnswerValue))]
+[JsonSerializable(typeof(ChatInputSelectedManyAnswerValue))]
+[JsonSerializable(typeof(ChatInputSingleSelectQuestion))]
+[JsonSerializable(typeof(ChatInputSkipped))]
+[JsonSerializable(typeof(ChatInputTextAnswerValue))]
+[JsonSerializable(typeof(ChatInputTextQuestion))]
+[JsonSerializable(typeof(ChatInteractivity))]
+[JsonSerializable(typeof(ChatOrigin))]
+[JsonSerializable(typeof(ChatOriginFork))]
+[JsonSerializable(typeof(ChatOriginKind))]
+[JsonSerializable(typeof(ChatOriginSideChat))]
+[JsonSerializable(typeof(ChatOriginTool))]
+[JsonSerializable(typeof(ChatOriginUser))]
+[JsonSerializable(typeof(ChatPendingMessageRemovedAction))]
+[JsonSerializable(typeof(ChatPendingMessageSetAction))]
+[JsonSerializable(typeof(ChatQueuedMessagesReorderedAction))]
+[JsonSerializable(typeof(ChatReasoningAction))]
+[JsonSerializable(typeof(ChatResponsePartAction))]
+[JsonSerializable(typeof(ChatSource))]
+[JsonSerializable(typeof(ChatSourceKind))]
+[JsonSerializable(typeof(ChatState))]
+[JsonSerializable(typeof(ChatSummary))]
+[JsonSerializable(typeof(ChatToolCallAuthRequiredAction))]
+[JsonSerializable(typeof(ChatToolCallAuthResolvedAction))]
+[JsonSerializable(typeof(ChatToolCallCompleteAction))]
+[JsonSerializable(typeof(ChatToolCallConfirmedAction))]
+[JsonSerializable(typeof(ChatToolCallContentChangedAction))]
+[JsonSerializable(typeof(ChatToolCallDeltaAction))]
+[JsonSerializable(typeof(ChatToolCallReadyAction))]
+[JsonSerializable(typeof(ChatToolCallResultConfirmedAction))]
+[JsonSerializable(typeof(ChatToolCallStartAction))]
+[JsonSerializable(typeof(ChatTruncatedAction))]
+[JsonSerializable(typeof(ChatTurnCancelledAction))]
+[JsonSerializable(typeof(ChatTurnCompleteAction))]
+[JsonSerializable(typeof(ChatTurnsLoadedAction))]
+[JsonSerializable(typeof(ChatTurnStartedAction))]
+[JsonSerializable(typeof(ChatUsageAction))]
+[JsonSerializable(typeof(ChatWorkingDirectoryRemovedAction))]
+[JsonSerializable(typeof(ChatWorkingDirectorySetAction))]
+[JsonSerializable(typeof(ChildCustomization))]
+[JsonSerializable(typeof(ClientCapabilities))]
+[JsonSerializable(typeof(ClientPluginCustomization))]
+[JsonSerializable(typeof(CompletionItem))]
+[JsonSerializable(typeof(CompletionItemKind))]
+[JsonSerializable(typeof(CompletionsParams))]
+[JsonSerializable(typeof(CompletionsResult))]
+[JsonSerializable(typeof(ConfigPropertySchema))]
+[JsonSerializable(typeof(ConfigSchema))]
+[JsonSerializable(typeof(ConfirmationOption))]
+[JsonSerializable(typeof(ConfirmationOptionKind))]
+[JsonSerializable(typeof(ContentEncoding))]
+[JsonSerializable(typeof(ContentRef))]
+[JsonSerializable(typeof(CreateChatParams))]
+[JsonSerializable(typeof(CreateResourceWatchParams))]
+[JsonSerializable(typeof(CreateResourceWatchResult))]
+[JsonSerializable(typeof(CreateSessionParams))]
+[JsonSerializable(typeof(CreateTerminalParams))]
+[JsonSerializable(typeof(Customization))]
+[JsonSerializable(typeof(CustomizationDegradedState))]
+[JsonSerializable(typeof(CustomizationEnablement))]
+[JsonSerializable(typeof(CustomizationEnablementGlobal))]
+[JsonSerializable(typeof(CustomizationEnablementKind))]
+[JsonSerializable(typeof(CustomizationEnablementSession))]
+[JsonSerializable(typeof(CustomizationEnablementWorkspace))]
+[JsonSerializable(typeof(CustomizationErrorState))]
+[JsonSerializable(typeof(CustomizationLoadedState))]
+[JsonSerializable(typeof(CustomizationLoadingState))]
+[JsonSerializable(typeof(CustomizationLoadState))]
+[JsonSerializable(typeof(CustomizationLoadStatus))]
+[JsonSerializable(typeof(CustomizationType))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(DirectoryCustomization))]
+[JsonSerializable(typeof(DirectoryEntry))]
+[JsonSerializable(typeof(DispatchActionParams))]
+[JsonSerializable(typeof(DisposeChatParams))]
+[JsonSerializable(typeof(DisposeSessionParams))]
+[JsonSerializable(typeof(DisposeTerminalParams))]
+[JsonSerializable(typeof(ErrorInfo))]
+[JsonSerializable(typeof(FetchAutomationRunsParams))]
+[JsonSerializable(typeof(FetchAutomationRunsResult))]
+[JsonSerializable(typeof(FetchTurnsParams))]
+[JsonSerializable(typeof(FetchTurnsResult))]
+[JsonSerializable(typeof(FileEdit))]
+[JsonSerializable(typeof(ForkChatSource))]
+[JsonSerializable(typeof(HookCustomization))]
+[JsonSerializable(typeof(Icon))]
+[JsonSerializable(typeof(Implementation))]
+[JsonSerializable(typeof(InitializeParams))]
+[JsonSerializable(typeof(InitializeResult))]
+[JsonSerializable(typeof(InputRequestResponsePart))]
+[JsonSerializable(typeof(InvokeChangesetOperationParams))]
+[JsonSerializable(typeof(InvokeChangesetOperationResult))]
+[JsonSerializable(typeof(JsonRpcErrorObject))]
+[JsonSerializable(typeof(JsonRpcErrorResponse))]
+[JsonSerializable(typeof(JsonRpcMessage))]
+[JsonSerializable(typeof(JsonRpcNotification))]
+[JsonSerializable(typeof(JsonRpcRequest))]
+[JsonSerializable(typeof(JsonRpcSuccessResponse))]
+[JsonSerializable(typeof(ListAutomationTriggerDefinitionsParams))]
+[JsonSerializable(typeof(ListAutomationTriggerDefinitionsResult))]
+[JsonSerializable(typeof(ListSessionsParams))]
+[JsonSerializable(typeof(ListSessionsResult))]
+[JsonSerializable(typeof(MarkdownResponsePart))]
+[JsonSerializable(typeof(McpAuthRequiredReason))]
+[JsonSerializable(typeof(McpAuthRequirement))]
+[JsonSerializable(typeof(McpOAuthClient))]
+[JsonSerializable(typeof(McpServerAuthRequiredState))]
+[JsonSerializable(typeof(McpServerCustomization))]
+[JsonSerializable(typeof(McpServerCustomizationApps))]
+[JsonSerializable(typeof(McpServerErrorState))]
+[JsonSerializable(typeof(McpServerReadyState))]
+[JsonSerializable(typeof(McpServerStartingState))]
+[JsonSerializable(typeof(McpServerState))]
+[JsonSerializable(typeof(McpServerStatus))]
+[JsonSerializable(typeof(McpServerStoppedState))]
+[JsonSerializable(typeof(Message))]
+[JsonSerializable(typeof(MessageAnnotationsAttachment))]
+[JsonSerializable(typeof(MessageAttachment))]
+[JsonSerializable(typeof(MessageAttachmentKind))]
+[JsonSerializable(typeof(MessageChatAttachment))]
+[JsonSerializable(typeof(MessageEmbeddedResourceAttachment))]
+[JsonSerializable(typeof(MessageKind))]
+[JsonSerializable(typeof(MessageOrigin))]
+[JsonSerializable(typeof(MessageResourceAttachment))]
+[JsonSerializable(typeof(ModelSelection))]
+[JsonSerializable(typeof(MultipleChatsCapability))]
+[JsonSerializable(typeof(MultipleWorkingDirectoriesCapability))]
+[JsonSerializable(typeof(OtlpExportLogsParams))]
+[JsonSerializable(typeof(OtlpExportMetricsParams))]
+[JsonSerializable(typeof(OtlpExportTracesParams))]
+[JsonSerializable(typeof(PartialChatSummary))]
+[JsonSerializable(typeof(PartialSessionSummary))]
+[JsonSerializable(typeof(PendingMessage))]
+[JsonSerializable(typeof(PendingMessageKind))]
+[JsonSerializable(typeof(PermissionDeniedErrorData))]
+[JsonSerializable(typeof(PluginCustomization))]
+[JsonSerializable(typeof(PolicyState))]
+[JsonSerializable(typeof(ProgressParams))]
+[JsonSerializable(typeof(ProjectInfo))]
+[JsonSerializable(typeof(PromptCustomization))]
+[JsonSerializable(typeof(ProtectedResourceMetadata))]
+[JsonSerializable(typeof(ReasoningResponsePart))]
+[JsonSerializable(typeof(ReconnectParams))]
+[JsonSerializable(typeof(ReconnectReplayResult))]
+[JsonSerializable(typeof(ReconnectResult))]
+[JsonSerializable(typeof(ReconnectResultType))]
+[JsonSerializable(typeof(ReconnectSnapshotResult))]
+[JsonSerializable(typeof(ResolveSessionConfigParams))]
+[JsonSerializable(typeof(ResolveSessionConfigResult))]
+[JsonSerializable(typeof(ResourceChange))]
+[JsonSerializable(typeof(ResourceChangeType))]
+[JsonSerializable(typeof(ResourceCopyParams))]
+[JsonSerializable(typeof(ResourceCopyResult))]
+[JsonSerializable(typeof(ResourceDeleteParams))]
+[JsonSerializable(typeof(ResourceDeleteResult))]
+[JsonSerializable(typeof(ResourceListParams))]
+[JsonSerializable(typeof(ResourceListResult))]
+[JsonSerializable(typeof(ResourceMkdirParams))]
+[JsonSerializable(typeof(ResourceMkdirResult))]
+[JsonSerializable(typeof(ResourceMoveParams))]
+[JsonSerializable(typeof(ResourceMoveResult))]
+[JsonSerializable(typeof(ResourceReadParams))]
+[JsonSerializable(typeof(ResourceReadResult))]
+[JsonSerializable(typeof(ResourceRequestParams))]
+[JsonSerializable(typeof(ResourceRequestResult))]
+[JsonSerializable(typeof(ResourceResolveParams))]
+[JsonSerializable(typeof(ResourceResolveResult))]
+[JsonSerializable(typeof(ResourceResponsePart))]
+[JsonSerializable(typeof(ResourceType))]
+[JsonSerializable(typeof(ResourceWatchChangedAction))]
+[JsonSerializable(typeof(ResourceWatchState))]
+[JsonSerializable(typeof(ResourceWriteMode))]
+[JsonSerializable(typeof(ResourceWriteParams))]
+[JsonSerializable(typeof(ResourceWriteResult))]
+[JsonSerializable(typeof(ResponsePart))]
+[JsonSerializable(typeof(ResponsePartKind))]
+[JsonSerializable(typeof(RootActiveSessionsChangedAction))]
+[JsonSerializable(typeof(RootAgentsChangedAction))]
+[JsonSerializable(typeof(RootConfigChangedAction))]
+[JsonSerializable(typeof(RootConfigState))]
+[JsonSerializable(typeof(RootState))]
+[JsonSerializable(typeof(RootTerminalsChangedAction))]
+[JsonSerializable(typeof(RuleCustomization))]
+[JsonSerializable(typeof(RunAutomationParams))]
+[JsonSerializable(typeof(RunAutomationResult))]
+[JsonSerializable(typeof(SessionActiveClient))]
+[JsonSerializable(typeof(SessionActiveClientRemovedAction))]
+[JsonSerializable(typeof(SessionActiveClientSetAction))]
+[JsonSerializable(typeof(SessionActivityChangedAction))]
+[JsonSerializable(typeof(SessionAddedParams))]
+[JsonSerializable(typeof(SessionChangesetsChangedAction))]
+[JsonSerializable(typeof(SessionChatAddedAction))]
+[JsonSerializable(typeof(SessionChatInputRequest))]
+[JsonSerializable(typeof(SessionChatRemovedAction))]
+[JsonSerializable(typeof(SessionChatUpdatedAction))]
+[JsonSerializable(typeof(SessionConfigChangedAction))]
+[JsonSerializable(typeof(SessionConfigCompletionsParams))]
+[JsonSerializable(typeof(SessionConfigCompletionsResult))]
+[JsonSerializable(typeof(SessionConfigPropertySchema))]
+[JsonSerializable(typeof(SessionConfigSchema))]
+[JsonSerializable(typeof(SessionConfigState))]
+[JsonSerializable(typeof(SessionConfigValueItem))]
+[JsonSerializable(typeof(SessionCreationFailedAction))]
+[JsonSerializable(typeof(SessionCustomizationRemovedAction))]
+[JsonSerializable(typeof(SessionCustomizationsChangedAction))]
+[JsonSerializable(typeof(SessionCustomizationToggledAction))]
+[JsonSerializable(typeof(SessionCustomizationUpdatedAction))]
+[JsonSerializable(typeof(SessionDefaultChatChangedAction))]
+[JsonSerializable(typeof(SessionDeltaAction))]
+[JsonSerializable(typeof(SessionErrorAction))]
+[JsonSerializable(typeof(SessionInputNeededRemovedAction))]
+[JsonSerializable(typeof(SessionInputNeededSetAction))]
+[JsonSerializable(typeof(SessionInputRequest))]
+[JsonSerializable(typeof(SessionInputRequestKind))]
+[JsonSerializable(typeof(SessionIsArchivedChangedAction))]
+[JsonSerializable(typeof(SessionIsReadChangedAction))]
+[JsonSerializable(typeof(SessionLifecycle))]
+[JsonSerializable(typeof(SessionMcpServerStartRequestedAction))]
+[JsonSerializable(typeof(SessionMcpServerStateChangedAction))]
+[JsonSerializable(typeof(SessionMcpServerStopRequestedAction))]
+[JsonSerializable(typeof(SessionMetaChangedAction))]
+[JsonSerializable(typeof(SessionModelInfo))]
+[JsonSerializable(typeof(SessionOrigin))]
+[JsonSerializable(typeof(SessionOriginKind))]
+[JsonSerializable(typeof(SessionPendingMessageRemovedAction))]
+[JsonSerializable(typeof(SessionPendingMessageSetAction))]
+[JsonSerializable(typeof(SessionQueuedMessagesReorderedAction))]
+[JsonSerializable(typeof(SessionReadyAction))]
+[JsonSerializable(typeof(SessionReasoningAction))]
+[JsonSerializable(typeof(SessionRemovedParams))]
+[JsonSerializable(typeof(SessionResponsePartAction))]
+[JsonSerializable(typeof(SessionServerToolsChangedAction))]
+[JsonSerializable(typeof(SessionState))]
+[JsonSerializable(typeof(SessionStatus))]
+[JsonSerializable(typeof(SessionSummary))]
+[JsonSerializable(typeof(SessionSummaryChangedParams))]
+[JsonSerializable(typeof(SessionTitleChangedAction))]
+[JsonSerializable(typeof(SessionToolAuthenticationRequest))]
+[JsonSerializable(typeof(SessionToolCallCompleteAction))]
+[JsonSerializable(typeof(SessionToolCallConfirmedAction))]
+[JsonSerializable(typeof(SessionToolCallContentChangedAction))]
+[JsonSerializable(typeof(SessionToolCallDeltaAction))]
+[JsonSerializable(typeof(SessionToolCallReadyAction))]
+[JsonSerializable(typeof(SessionToolCallResultConfirmedAction))]
+[JsonSerializable(typeof(SessionToolCallStartAction))]
+[JsonSerializable(typeof(SessionToolClientExecutionRequest))]
+[JsonSerializable(typeof(SessionToolConfirmationRequest))]
+[JsonSerializable(typeof(SessionTruncatedAction))]
+[JsonSerializable(typeof(SessionTurnCancelledAction))]
+[JsonSerializable(typeof(SessionTurnCompleteAction))]
+[JsonSerializable(typeof(SessionTurnStartedAction))]
+[JsonSerializable(typeof(SessionUsageAction))]
+[JsonSerializable(typeof(SessionWorkingDirectoryRemovedAction))]
+[JsonSerializable(typeof(SessionWorkingDirectoryReplacedAction))]
+[JsonSerializable(typeof(SessionWorkingDirectorySetAction))]
+[JsonSerializable(typeof(SideChatSelection))]
+[JsonSerializable(typeof(SideChatSource))]
+[JsonSerializable(typeof(SimpleMessageAttachment))]
+[JsonSerializable(typeof(SkillCustomization))]
+[JsonSerializable(typeof(Snapshot))]
+[JsonSerializable(typeof(SnapshotState))]
+[JsonSerializable(typeof(StateAction))]
+[JsonSerializable(typeof(StringOrMarkdown))]
+[JsonSerializable(typeof(SubscribeParams))]
+[JsonSerializable(typeof(SubscribeResult))]
+[JsonSerializable(typeof(SubscribeView))]
+[JsonSerializable(typeof(SubscriptionDeliveryOptions))]
+[JsonSerializable(typeof(SystemNotificationResponsePart))]
+[JsonSerializable(typeof(TelemetryCapabilities))]
+[JsonSerializable(typeof(TerminalClaim))]
+[JsonSerializable(typeof(TerminalClaimedAction))]
+[JsonSerializable(typeof(TerminalClaimKind))]
+[JsonSerializable(typeof(TerminalClearedAction))]
+[JsonSerializable(typeof(TerminalClientClaim))]
+[JsonSerializable(typeof(TerminalCommandDetectionAvailableAction))]
+[JsonSerializable(typeof(TerminalCommandExecutedAction))]
+[JsonSerializable(typeof(TerminalCommandFinishedAction))]
+[JsonSerializable(typeof(TerminalCommandPart))]
+[JsonSerializable(typeof(TerminalCommandResult))]
+[JsonSerializable(typeof(TerminalContentPart))]
+[JsonSerializable(typeof(TerminalCwdChangedAction))]
+[JsonSerializable(typeof(TerminalDataAction))]
+[JsonSerializable(typeof(TerminalExitedAction))]
+[JsonSerializable(typeof(TerminalExitedLifecycleState))]
+[JsonSerializable(typeof(TerminalInfo))]
+[JsonSerializable(typeof(TerminalInputAction))]
+[JsonSerializable(typeof(TerminalLifecycleState))]
+[JsonSerializable(typeof(TerminalLifecycleStatus))]
+[JsonSerializable(typeof(TerminalResizedAction))]
+[JsonSerializable(typeof(TerminalRunningLifecycleState))]
+[JsonSerializable(typeof(TerminalSessionClaim))]
+[JsonSerializable(typeof(TerminalState))]
+[JsonSerializable(typeof(TerminalTitleChangedAction))]
+[JsonSerializable(typeof(TerminalUnclassifiedPart))]
+[JsonSerializable(typeof(TextPosition))]
+[JsonSerializable(typeof(TextRange))]
+[JsonSerializable(typeof(TextSelection))]
+[JsonSerializable(typeof(ToolAnnotations))]
+[JsonSerializable(typeof(ToolCallAuthRequiredState))]
+[JsonSerializable(typeof(ToolCallCancellationReason))]
+[JsonSerializable(typeof(ToolCallCancelledState))]
+[JsonSerializable(typeof(ToolCallClientContributor))]
+[JsonSerializable(typeof(ToolCallCompletedState))]
+[JsonSerializable(typeof(ToolCallConfirmationReason))]
+[JsonSerializable(typeof(ToolCallConfirmationState))]
+[JsonSerializable(typeof(ToolCallContributor))]
+[JsonSerializable(typeof(ToolCallContributorKind))]
+[JsonSerializable(typeof(ToolCallMcpContributor))]
+[JsonSerializable(typeof(ToolCallPendingConfirmationState))]
+[JsonSerializable(typeof(ToolCallPendingResultConfirmationState))]
+[JsonSerializable(typeof(ToolCallResponsePart))]
+[JsonSerializable(typeof(ToolCallResult))]
+[JsonSerializable(typeof(ToolCallRiskAssessment))]
+[JsonSerializable(typeof(ToolCallRiskAssessmentCompleteState))]
+[JsonSerializable(typeof(ToolCallRiskAssessmentKind))]
+[JsonSerializable(typeof(ToolCallRiskAssessmentLoadingState))]
+[JsonSerializable(typeof(ToolCallRiskAssessmentStatus))]
+[JsonSerializable(typeof(ToolCallRunningState))]
+[JsonSerializable(typeof(ToolCallState))]
+[JsonSerializable(typeof(ToolCallStatus))]
+[JsonSerializable(typeof(ToolCallStreamingState))]
+[JsonSerializable(typeof(ToolDefinition))]
+[JsonSerializable(typeof(ToolInput))]
+[JsonSerializable(typeof(ToolResultContent))]
+[JsonSerializable(typeof(ToolResultContentType))]
+[JsonSerializable(typeof(ToolResultEmbeddedResourceContent))]
+[JsonSerializable(typeof(ToolResultFileEditContent))]
+[JsonSerializable(typeof(ToolResultResourceContent))]
+[JsonSerializable(typeof(ToolResultSubagentContent))]
+[JsonSerializable(typeof(ToolResultTerminalContent))]
+[JsonSerializable(typeof(ToolResultTextContent))]
+[JsonSerializable(typeof(Turn))]
+[JsonSerializable(typeof(TurnState))]
+[JsonSerializable(typeof(UnsubscribeParams))]
+[JsonSerializable(typeof(UnsupportedProtocolVersionErrorData))]
+[JsonSerializable(typeof(UsageInfo))]
+[JsonSourceGenerationOptions(
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+ GenerationMode = JsonSourceGenerationMode.Metadata)]
+internal partial class AgentHostProtocolJsonContext : JsonSerializerContext
+{
+}
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Messages.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Messages.generated.cs
index d00905ef7..39af8b151 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Messages.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Messages.generated.cs
@@ -103,19 +103,19 @@ public override JsonRpcMessage Read(ref Utf8JsonReader reader, Type typeToConver
var msg = new JsonRpcMessage();
if (hasMethod && hasId)
{
- msg.Request = root.Deserialize(options);
+ msg.Request = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (hasMethod)
{
- msg.Notification = root.Deserialize(options);
+ msg.Notification = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (hasError)
{
- msg.ErrorResponse = root.Deserialize(options);
+ msg.ErrorResponse = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (hasResult)
{
- msg.SuccessResponse = root.Deserialize(options);
+ msg.SuccessResponse = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else
{
@@ -126,10 +126,10 @@ public override JsonRpcMessage Read(ref Utf8JsonReader reader, Type typeToConver
public override void Write(Utf8JsonWriter writer, JsonRpcMessage value, JsonSerializerOptions options)
{
- if (value.Request is not null) { JsonSerializer.Serialize(writer, value.Request, options); return; }
- if (value.SuccessResponse is not null) { JsonSerializer.Serialize(writer, value.SuccessResponse, options); return; }
- if (value.ErrorResponse is not null) { JsonSerializer.Serialize(writer, value.ErrorResponse, options); return; }
- if (value.Notification is not null) { JsonSerializer.Serialize(writer, value.Notification, options); return; }
+ if (value.Request is not null) { JsonSerializer.Serialize(writer, value.Request, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.SuccessResponse is not null) { JsonSerializer.Serialize(writer, value.SuccessResponse, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.ErrorResponse is not null) { JsonSerializer.Serialize(writer, value.ErrorResponse, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Notification is not null) { JsonSerializer.Serialize(writer, value.Notification, AhpJsonTypeInfo.Get(options)); return; }
writer.WriteNullValue();
}
}
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index b9173a034..532cdb4f0 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -6210,13 +6210,22 @@ public override ToolInput Read(ref Utf8JsonReader reader, Type typeToConvert, Js
{
return new ToolInput { Inline = reader.GetString() };
}
- return new ToolInput { ContentRef = JsonSerializer.Deserialize(ref reader, options) };
+ return new ToolInput
+ {
+ ContentRef = JsonSerializer.Deserialize(
+ ref reader,
+ AhpJsonTypeInfo.Get(options))
+ };
}
public override void Write(Utf8JsonWriter writer, ToolInput value, JsonSerializerOptions options)
{
if (value.Inline is not null) { writer.WriteStringValue(value.Inline); return; }
- if (value.ContentRef is not null) { JsonSerializer.Serialize(writer, value.ContentRef, options); return; }
+ if (value.ContentRef is not null)
+ {
+ JsonSerializer.Serialize(writer, value.ContentRef, AhpJsonTypeInfo.Get(options));
+ return;
+ }
writer.WriteNullValue();
}
}
@@ -6271,57 +6280,57 @@ public override SnapshotState Read(ref Utf8JsonReader reader, Type typeToConvert
root.TryGetProperty("origin", out _) &&
root.TryGetProperty("sessions", out _))
{
- result.AutomationRun = root.Deserialize(options);
+ result.AutomationRun = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("automations", out _))
{
- result.Automations = root.Deserialize(options);
+ result.Automations = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("turns", out _))
{
- result.Chat = root.Deserialize(options);
+ result.Chat = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("lifecycle", out _))
{
// SessionState is discriminated on its required `lifecycle` field.
// (It no longer carries `summary`; that field was removed when the
// session state was flattened.)
- result.Session = root.Deserialize(options);
+ result.Session = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("content", out _))
{
- result.Terminal = root.Deserialize(options);
+ result.Terminal = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("status", out _) && root.TryGetProperty("files", out _))
{
- result.Changeset = root.Deserialize(options);
+ result.Changeset = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("root", out _) && root.TryGetProperty("recursive", out _))
{
- result.ResourceWatch = root.Deserialize(options);
+ result.ResourceWatch = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else if (root.TryGetProperty("annotations", out _))
{
- result.Annotations = root.Deserialize(options);
+ result.Annotations = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
else
{
- result.Root = root.Deserialize(options);
+ result.Root = root.Deserialize(AhpJsonTypeInfo.Get(options));
}
return result;
}
public override void Write(Utf8JsonWriter writer, SnapshotState value, JsonSerializerOptions options)
{
- if (value.AutomationRun is not null) { JsonSerializer.Serialize(writer, value.AutomationRun, options); return; }
- if (value.Automations is not null) { JsonSerializer.Serialize(writer, value.Automations, options); return; }
- if (value.Chat is not null) { JsonSerializer.Serialize(writer, value.Chat, options); return; }
- if (value.Session is not null) { JsonSerializer.Serialize(writer, value.Session, options); return; }
- if (value.Terminal is not null) { JsonSerializer.Serialize(writer, value.Terminal, options); return; }
- if (value.Changeset is not null) { JsonSerializer.Serialize(writer, value.Changeset, options); return; }
- if (value.ResourceWatch is not null) { JsonSerializer.Serialize(writer, value.ResourceWatch, options); return; }
- if (value.Annotations is not null) { JsonSerializer.Serialize(writer, value.Annotations, options); return; }
- if (value.Root is not null) { JsonSerializer.Serialize(writer, value.Root, options); return; }
+ if (value.AutomationRun is not null) { JsonSerializer.Serialize(writer, value.AutomationRun, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Automations is not null) { JsonSerializer.Serialize(writer, value.Automations, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Chat is not null) { JsonSerializer.Serialize(writer, value.Chat, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Session is not null) { JsonSerializer.Serialize(writer, value.Session, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Terminal is not null) { JsonSerializer.Serialize(writer, value.Terminal, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Changeset is not null) { JsonSerializer.Serialize(writer, value.Changeset, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.ResourceWatch is not null) { JsonSerializer.Serialize(writer, value.ResourceWatch, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Annotations is not null) { JsonSerializer.Serialize(writer, value.Annotations, AhpJsonTypeInfo.Get(options)); return; }
+ if (value.Root is not null) { JsonSerializer.Serialize(writer, value.Root, AhpJsonTypeInfo.Get(options)); return; }
writer.WriteNullValue();
}
}
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/AhpJsonMetadata.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/AhpJsonMetadata.cs
new file mode 100644
index 000000000..aada60eae
--- /dev/null
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/AhpJsonMetadata.cs
@@ -0,0 +1,12 @@
+#nullable enable
+
+using System.Text.Json.Serialization.Metadata;
+
+namespace Microsoft.AgentHostProtocol;
+
+/// Provides source-generated System.Text.Json metadata for the complete AHP wire model.
+public static class AhpJsonMetadata
+{
+ /// Gets the source-generated resolver for AHP protocol types.
+ public static IJsonTypeInfoResolver Default => AgentHostProtocolJsonContext.Default;
+}
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/AhpJsonTypeInfo.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/AhpJsonTypeInfo.cs
new file mode 100644
index 000000000..81d0457d9
--- /dev/null
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/AhpJsonTypeInfo.cs
@@ -0,0 +1,13 @@
+#nullable enable
+
+using System.Text.Json.Serialization.Metadata;
+
+namespace Microsoft.AgentHostProtocol;
+
+internal static class AhpJsonTypeInfo
+{
+ public static JsonTypeInfo Get(JsonSerializerOptions options) =>
+ options.GetTypeInfo(typeof(T)) as JsonTypeInfo
+ ?? throw new NotSupportedException(
+ $"No JSON metadata is registered for {typeof(T)}.");
+}
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/IAhpSerializer.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/IAhpSerializer.cs
index 806c976f6..d1a91e262 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/IAhpSerializer.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/IAhpSerializer.cs
@@ -4,7 +4,6 @@
#nullable enable
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace Microsoft.AgentHostProtocol;
@@ -20,8 +19,6 @@ namespace Microsoft.AgentHostProtocol;
public interface IAhpSerializer
{
/// Serializes to a JSON string.
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
string Serialize(T value);
///
@@ -30,18 +27,12 @@ public interface IAhpSerializer
/// undisposed-document leak that JsonDocument.Parse(Serialize(x)).RootElement
/// incurs). The returned element owns its backing memory and is safe to retain.
///
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
JsonElement SerializeToElement(T value);
/// Deserializes a JSON string into .
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
T Deserialize(string json);
/// Deserializes UTF-8 JSON bytes into .
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
T Deserialize(ReadOnlySpan utf8Json);
///
@@ -52,36 +43,14 @@ public interface IAhpSerializer
/// Deserialize<T>(element.GetRawText()) on hot paths (inbound
/// notifications, request results) where the element is already in hand.
///
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
T Deserialize(JsonElement element);
///
/// Decodes a transport frame into a , picking the
/// correct variant (request / notification / success / error) from its shape.
///
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
JsonRpcMessage DecodeMessage(TransportMessage message);
/// Encodes a into a text transport frame.
- [RequiresUnreferencedCode(SerializerTrimWarnings.UnreferencedCode)]
- [RequiresDynamicCode(SerializerTrimWarnings.DynamicCode)]
TransportMessage EncodeMessage(JsonRpcMessage message);
}
-
-///
-/// Shared /
-/// messages for the serializer seam.
-/// The default SystemTextJsonAhpSerializer is reflection-based (source-gen
-/// is deferred per docs/decisions/serialization.md), so every
-/// (de)serialization entry point declares the trim/AOT unsafety on the contract.
-///
-internal static class SerializerTrimWarnings
-{
- public const string UnreferencedCode =
- "JSON (de)serialization here is reflection-based and may reference types that cannot be statically analyzed when trimming. Provide a JsonSerializerContext or preserve the wire types.";
-
- public const string DynamicCode =
- "JSON (de)serialization here is reflection-based and may require runtime code generation under Native AOT. Use System.Text.Json source generation for AOT.";
-}
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/StringOrMarkdown.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/StringOrMarkdown.cs
index c596c6e97..ddb5bb68d 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/StringOrMarkdown.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/StringOrMarkdown.cs
@@ -20,21 +20,29 @@ public sealed class StringOrMarkdown
/// Non-null iff the value was decoded from the { "markdown": "..." }
/// object form.
///
- public string? Markdown { get; init; }
+ public string? Markdown { get; }
/// Non-null iff the value was decoded from a bare JSON string.
- public string? Plain { get; init; }
+ public string? Plain { get; }
/// Creates an empty value (encodes as "").
public StringOrMarkdown()
{
}
+ private StringOrMarkdown(string? plain, string? markdown)
+ {
+ Plain = plain;
+ Markdown = markdown;
+ }
+
/// Returns a value that encodes as a bare JSON string.
- public static StringOrMarkdown FromPlain(string text) => new() { Plain = text };
+ public static StringOrMarkdown FromPlain(string text) =>
+ new(text ?? throw new ArgumentNullException(nameof(text)), markdown: null);
/// Returns a value that encodes as { "markdown": text }.
- public static StringOrMarkdown FromMarkdown(string text) => new() { Markdown = text };
+ public static StringOrMarkdown FromMarkdown(string text) =>
+ new(plain: null, text ?? throw new ArgumentNullException(nameof(text)));
///
/// Returns the underlying text regardless of which form the value was
@@ -54,7 +62,7 @@ public override StringOrMarkdown Read(ref Utf8JsonReader reader, Type typeToConv
case JsonTokenType.Null:
return new StringOrMarkdown();
case JsonTokenType.String:
- return new StringOrMarkdown { Plain = reader.GetString() };
+ return StringOrMarkdown.FromPlain(reader.GetString()!);
default:
using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
{
@@ -62,7 +70,7 @@ public override StringOrMarkdown Read(ref Utf8JsonReader reader, Type typeToConv
&& doc.RootElement.TryGetProperty("markdown", out JsonElement md)
&& md.ValueKind == JsonValueKind.String)
{
- return new StringOrMarkdown { Markdown = md.GetString() };
+ return StringOrMarkdown.FromMarkdown(md.GetString()!);
}
throw new JsonException("StringOrMarkdown object form missing required 'markdown' field");
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/UnionConverter.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/UnionConverter.cs
index 588fc8f9b..f150688ee 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/UnionConverter.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Json/UnionConverter.cs
@@ -4,9 +4,9 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
namespace Microsoft.AgentHostProtocol;
@@ -42,17 +42,6 @@ protected UnionConverter(
}
///
- // root.Deserialize(variantType, options) resolves the payload type at runtime
- // from the variant map — genuinely trim/AOT-unsafe. The unsafety is already
- // declared on the public contract (IAhpSerializer is [RequiresUnreferencedCode]/
- // [RequiresDynamicCode]); this converter is reachable ONLY through that
- // serializer. JsonConverter.Read in the base is not annotated, so the
- // requirement cannot be re-declared via [RequiresUnreferencedCode] here (it
- // would trip IL2046) — the suppression points back to the contract that owns it.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "Reached only via the [RequiresUnreferencedCode] IAhpSerializer; the base JsonConverter.Read cannot carry the attribute.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "Reached only via the [RequiresDynamicCode] IAhpSerializer; the base JsonConverter.Read cannot carry the attribute.")]
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
@@ -74,7 +63,8 @@ protected UnionConverter(
var result = new T();
if (disc is not null && _variants.TryGetValue(disc, out Type? variantType))
{
- result.Value = root.Deserialize(variantType, options);
+ JsonTypeInfo typeInfo = options.GetTypeInfo(variantType);
+ result.Value = JsonSerializer.Deserialize(root, typeInfo);
}
else if (_allowUnknown)
{
@@ -91,14 +81,6 @@ protected UnionConverter(
}
///
- // JsonSerializer.Serialize(writer, inner, inner.GetType(), options) serializes
- // by the boxed runtime type — genuinely trim/AOT-unsafe, for the same reason
- // as Read above. Declared on the IAhpSerializer contract; suppressed here
- // because the base JsonConverter.Write is not annotated.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "Reached only via the [RequiresUnreferencedCode] IAhpSerializer; the base JsonConverter.Write cannot carry the attribute.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "Reached only via the [RequiresDynamicCode] IAhpSerializer; the base JsonConverter.Write cannot carry the attribute.")]
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
object? inner = value?.Value;
@@ -117,6 +99,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
// Serialize by the runtime type so every property (including the
// variant's own discriminator field) is written.
- JsonSerializer.Serialize(writer, inner, inner.GetType(), options);
+ JsonTypeInfo typeInfo = options.GetTypeInfo(inner.GetType());
+ JsonSerializer.Serialize(writer, inner, typeInfo);
}
}
diff --git a/clients/dotnet/src/AgentHostProtocol/AgentHostProtocol.csproj b/clients/dotnet/src/AgentHostProtocol/AgentHostProtocol.csproj
index cb44c7f00..51bd1754b 100644
--- a/clients/dotnet/src/AgentHostProtocol/AgentHostProtocol.csproj
+++ b/clients/dotnet/src/AgentHostProtocol/AgentHostProtocol.csproj
@@ -7,14 +7,10 @@
trueMicrosoft.AgentHostProtocolThe Agent Host Protocol (AHP) client for .NET: an async JSON-RPC client, the pure state reducers, the default System.Text.Json serializer, a ClientWebSocket transport, and the multi-host runtime.
-
+
+ true
+ truetruetrue
@@ -33,6 +29,7 @@
+
diff --git a/clients/dotnet/src/AgentHostProtocol/AhpClient.cs b/clients/dotnet/src/AgentHostProtocol/AhpClient.cs
index ab1998f60..e90eb47d0 100644
--- a/clients/dotnet/src/AgentHostProtocol/AhpClient.cs
+++ b/clients/dotnet/src/AgentHostProtocol/AhpClient.cs
@@ -52,8 +52,14 @@ private KeepAlivePolicy(bool isEnabled, TimeSpan interval, TimeSpan timeout)
/// Periodically send a transport-level ping. Mirrors Swift
/// .ping(interval:timeout:).
///
- public static KeepAlivePolicy Ping(TimeSpan interval, TimeSpan timeout) =>
- new(isEnabled: true, interval: interval, timeout: timeout);
+ public static KeepAlivePolicy Ping(TimeSpan interval, TimeSpan timeout)
+ {
+ if (interval <= TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(nameof(interval), interval, "Keep-alive interval must be positive.");
+ if (timeout <= TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(nameof(timeout), timeout, "Keep-alive timeout must be positive.");
+ return new(isEnabled: true, interval: interval, timeout: timeout);
+ }
///
/// Convenience for the common WebSocket ping policy (30 s interval, 5 s
@@ -72,6 +78,13 @@ public sealed class ClientConfig
///
public TimeSpan DefaultRequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
+ ///
+ /// Supplies time for request timeouts, keep-alive scheduling, reconnect
+ /// backoff, and host timestamps. Defaults to .
+ /// Override in tests to advance time deterministically.
+ ///
+ public TimeProvider TimeProvider { get; set; } = TimeProvider.System;
+
///
/// Capacity of each subscription's event channel. Excess events are dropped
/// on a full channel (mirrors Go's SubscriptionBuffer). Defaults to 256.
@@ -87,6 +100,20 @@ public sealed class ClientConfig
/// Returns a config with sensible defaults (30 s timeout, 256-message buffer).
public static ClientConfig Default => new();
+
+ internal static ClientConfig Snapshot(ClientConfig? config)
+ {
+ var source = config ?? Default;
+ return new ClientConfig
+ {
+ DefaultRequestTimeout = source.DefaultRequestTimeout,
+ TimeProvider = source.TimeProvider ?? TimeProvider.System,
+ SubscriptionBufferCapacity = source.SubscriptionBufferCapacity > 0
+ ? source.SubscriptionBufferCapacity
+ : 256,
+ KeepAlive = source.KeepAlive ?? KeepAlivePolicy.Disabled,
+ };
+ }
}
// ─── Connection state ──────────────────────────────────────────────────────────
@@ -226,11 +253,16 @@ private sealed class OutboundMessage
{
public JsonRpcMessage Message { get; }
public TaskCompletionSource? Sent { get; }
+ public CancellationToken CancellationToken { get; }
- public OutboundMessage(JsonRpcMessage message, TaskCompletionSource? sent = null)
+ public OutboundMessage(
+ JsonRpcMessage message,
+ TaskCompletionSource? sent = null,
+ CancellationToken cancellationToken = default)
{
Message = message;
Sent = sent;
+ CancellationToken = cancellationToken;
}
}
@@ -254,14 +286,15 @@ private AhpClient(ITransport transport, ClientConfig cfg, IAhpSerializer seriali
// `startKeepAliveIfNeeded()` guard (`case .ping` + `as? AHPKeepAliveTransport`).
if (_cfg.KeepAlive.IsEnabled && _transport is IKeepAliveTransport pingTransport)
{
- _keepAliveTask = Task.Run(() => RunKeepAliveAsync(pingTransport));
+ _keepAliveTask = RunKeepAliveAsync(pingTransport);
}
}
///
/// Wires to a new and
/// starts the background reader / writer tasks. The client owns the transport
- /// from this point.
+ /// from this point. The configuration is snapshotted; subsequent changes to
+ /// do not affect the connected client.
///
public static AhpClient Connect(
ITransport transport,
@@ -269,8 +302,7 @@ public static AhpClient Connect(
IAhpSerializer? serializer = null)
{
Guard.ThrowIfNull(transport, nameof(transport));
- var cfg = config ?? ClientConfig.Default;
- if (cfg.SubscriptionBufferCapacity <= 0) cfg.SubscriptionBufferCapacity = 256;
+ var cfg = ClientConfig.Snapshot(config);
return new AhpClient(transport, cfg, serializer ?? SystemTextJsonAhpSerializer.Default);
}
@@ -472,7 +504,10 @@ private async Task RunKeepAliveAsync(IKeepAliveTransport pingTransport)
{
while (!ct.IsCancellationRequested)
{
- await Task.Delay(policy.Interval, ct).ConfigureAwait(false);
+ await TimeProviderCompatibility.DelayAsync(
+ _cfg.TimeProvider,
+ policy.Interval,
+ ct).ConfigureAwait(false);
if (ct.IsCancellationRequested) return;
await pingTransport.SendPingAsync(policy.Timeout, ct).ConfigureAwait(false);
}
@@ -496,27 +531,28 @@ await ShutdownWithErrorAsync(
// ── Writer loop ───────────────────────────────────────────────────────
- // The client routes all JSON through the injected IAhpSerializer, whose
- // contract is [RequiresUnreferencedCode]/[RequiresDynamicCode] (the default
- // SystemTextJsonAhpSerializer is reflection-based). The trim/AOT unsafety is
- // declared at that contract; re-declaring it on this internal loop — or
- // propagating the attribute up through the constructor / Connect() / the
- // entire client + multi-host public surface — is out of scope here, so the
- // serializer-call warnings are suppressed at the call site with that
- // contract named as the owner.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
private async Task RunWriterAsync()
{
try
{
await foreach (var item in _outbound.Reader.ReadAllAsync(_lifetimeCts.Token).ConfigureAwait(false))
{
+ if (item.CancellationToken.IsCancellationRequested)
+ {
+ item.Sent?.TrySetCanceled(item.CancellationToken);
+ continue;
+ }
+
var frame = _serializer.EncodeMessage(item.Message);
+ if (item.CancellationToken.IsCancellationRequested)
+ {
+ item.Sent?.TrySetCanceled(item.CancellationToken);
+ continue;
+ }
try
{
+ // Canceling ClientWebSocket.SendAsync aborts the shared connection,
+ // so message cancellation only prevents writes that have not started.
await _transport.SendAsync(frame, _lifetimeCts.Token).ConfigureAwait(false);
item.Sent?.TrySetResult(true);
}
@@ -545,13 +581,6 @@ private async Task RunWriterAsync()
// ── Reader loop ───────────────────────────────────────────────────────
- // See RunWriterAsync: JSON decode goes through the [RequiresUnreferencedCode]/
- // [RequiresDynamicCode] IAhpSerializer, which owns the trim/AOT-unsafety
- // declaration.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
private async Task RunReaderAsync()
{
try
@@ -654,12 +683,6 @@ private void Deliver(ulong id, JsonElement result, AhpRpcException? rpcError)
/// handler is installed, otherwise the handler's result (or its thrown error).
/// Mirrors the TS client's handleServerRequest.
///
- // See RunWriterAsync: the handler's result serialize goes through the
- // [RequiresUnreferencedCode]/[RequiresDynamicCode] IAhpSerializer contract.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
private async Task HandleServerRequestAsync(JsonRpcRequest req)
{
var handler = _serverRequestHandler;
@@ -715,12 +738,6 @@ private async Task EnqueueReplyAsync(JsonRpcMessage msg)
catch { /* shutting down — best effort */ }
}
- // See RunWriterAsync: the per-notification deserialize calls go through the
- // [RequiresUnreferencedCode]/[RequiresDynamicCode] IAhpSerializer contract.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
private void HandleNotification(JsonRpcNotification n)
{
if (n.Params is null) return;
@@ -821,12 +838,6 @@ private void FanOut(string channel, SubscriptionEvent ev)
/// would be non-null.
///
///
- // See RunWriterAsync: param serialize + result deserialize go through the
- // [RequiresUnreferencedCode]/[RequiresDynamicCode] IAhpSerializer contract.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
public async Task RequestAsync(
string method,
TParams parameters,
@@ -844,6 +855,17 @@ private void FanOut(string channel, SubscriptionEvent ev)
// minted and the pending entry is registered.
cancellationToken.ThrowIfCancellationRequested();
+ // Validate and arm the timeout before minting an id or registering pending
+ // state. If a provider rejects the duration, no bookkeeping can leak.
+ using var timeoutCts = _cfg.DefaultRequestTimeout > TimeSpan.Zero
+ ? TimeProviderCompatibility.CreateCancellationTokenSource(
+ _cfg.TimeProvider,
+ _cfg.DefaultRequestTimeout)
+ : null;
+ using var requestCts = timeoutCts is null
+ ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
+ : CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+
// Gate the span-name string-build on HasListeners so the no-listener path
// stays allocation-free; the method goes in the span name (OTel "{op} {target}")
// and as the rpc.method tag.
@@ -885,7 +907,7 @@ private void FanOut(string channel, SubscriptionEvent ev)
try
{
- await SendMessageAsync(req, cancellationToken).ConfigureAwait(false);
+ await SendMessageAsync(req, requestCts.Token).ConfigureAwait(false);
}
catch
{
@@ -894,15 +916,9 @@ private void FanOut(string channel, SubscriptionEvent ev)
}
AhpTelemetry.MessagesSent.Add(1, new KeyValuePair(AhpTelemetryNames.AttrMessageKind, AhpTelemetryNames.MessageKindRequest));
- // Always apply the configured default timeout when positive, composing it
- // with any caller-supplied cancellation token.
- using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- if (_cfg.DefaultRequestTimeout > TimeSpan.Zero)
- linkedCts.CancelAfter(_cfg.DefaultRequestTimeout);
-
try
{
- var resultEl = await tcs.Task.WaitAsync(linkedCts.Token).ConfigureAwait(false);
+ var resultEl = await tcs.Task.WaitAsync(requestCts.Token).ConfigureAwait(false);
activity?.SetStatus(ActivityStatusCode.Ok);
outcome = AhpTelemetryNames.OutcomeOk;
if (resultEl.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
@@ -944,12 +960,6 @@ private void FanOut(string channel, SubscriptionEvent ev)
///
/// Sends a JSON-RPC notification (fire-and-forget).
///
- // See RunWriterAsync: param serialize goes through the
- // [RequiresUnreferencedCode]/[RequiresDynamicCode] IAhpSerializer contract.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
public async Task NotifyAsync(
string method,
TParams parameters,
@@ -978,7 +988,7 @@ public async Task NotifyAsync(
private async Task SendMessageAsync(JsonRpcMessage msg, CancellationToken cancellationToken)
{
var sentTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- var item = new OutboundMessage(msg, sentTcs);
+ var item = new OutboundMessage(msg, sentTcs, cancellationToken);
try
{
@@ -1026,9 +1036,16 @@ public async Task InitializeAsync(
// The protocol requires a result for `initialize`; a null/empty result is
// a protocol violation, surfaced loudly rather than returned as null.
- return await RequestAsync("initialize", @params, cancellationToken)
+ var result = await RequestAsync("initialize", @params, cancellationToken)
.ConfigureAwait(false)
?? throw new AhpRpcException(JsonRpcErrorCodes.InternalError, "ahp: initialize returned no result");
+ if (!versions.Contains(result.ProtocolVersion))
+ {
+ throw new AhpTransportException(
+ "protocol",
+ $"ahp: server selected unoffered protocol version '{result.ProtocolVersion}'");
+ }
+ return result;
}
/// Re-establishes a dropped connection via the reconnect flow.
@@ -1340,10 +1357,6 @@ public void SetResourceRequestHandlers(ResourceRequestHandlers? handlers) =>
// Decode the raw params into the typed record and dispatch to the matching
// handler. An unset method rejects with MethodNotFound so the peer sees the
// same reply as the no-handler path.
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "JSON goes through the [RequiresUnreferencedCode] IAhpSerializer, which declares the reflection unsafety on its contract.")]
- [UnconditionalSuppressMessage("AOT", "IL3050",
- Justification = "JSON goes through the [RequiresDynamicCode] IAhpSerializer, which declares the AOT unsafety on its contract.")]
private async Task