Skip to content

Commit 1a11eca

Browse files
committed
Ship local IChatClient adapter for 04.2.4-anthropic-agents; sample now runs end-to-end
Anthropic.SDK 5.10.0 was compiled against Microsoft.Extensions.AI.Abstractions 10.3.0; the repo's central pin (10.5.0, required by Microsoft.Agents.AI 1.3) reshaped HostedMcpServerTool.AuthorizationToken, causing a runtime MissingMethodException through the SDK's bundled ChatClientHelper. A per-project VersionOverride to 10.3.0 does not work because Agents.AI 1.3 itself rejects 10.3 with CS1705 at compile time. Fix: AnthropicChatClient.cs -- a ~120-line IChatClient adapter that calls AnthropicClient.Messages.GetClaudeMessageAsync directly and translates between Anthropic wire types and Microsoft.Extensions.AI types, bypassing ChatClientHelper entirely. Program.cs constructs the adapter; README.md explains the rationale and removal trigger (Anthropic.SDK 5.11+ rebuilt against M.E.AI 10.5+). Verification log updated with a 2026-05-03 Green entry: two-turn live conversation against claude-haiku-4-5-20251001 completed without exception.
1 parent 919fe75 commit 1a11eca

4 files changed

Lines changed: 170 additions & 3 deletions

File tree

docs/verification-log.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,32 @@ Copy this block to start a new entry. Date format `YYYY-MM-DD`.
9494

9595
---
9696

97+
## 2026-05-03 -- Option-2 Anthropic shim landed; 04.2.4 sample now runs
98+
99+
**Status:** Green for the previously-yellow Anthropic chapter sample. Defect from 2026-05-02 cleared via path 2 (custom `IChatClient` adapter).
100+
101+
### Packages
102+
- [x] `Anthropic.SDK` -- still v5.10.0 (no 5.11+ on NuGet yet; checked via `dotnet package search Anthropic.SDK --exact-match`).
103+
- [x] `Microsoft.Extensions.AI` -- v10.5.0 central pin unchanged. No per-project `VersionOverride` on the chapter sample.
104+
105+
### Code samples
106+
- [x] `samples/ch04-agent-framework/04.2.4-anthropic-agents` builds clean against the central pins (no override needed).
107+
- [x] Live-API end-to-end run with `claude-haiku-4-5-20251001` -- both turns of the conversation produced real Claude output. No `MissingMethodException`.
108+
109+
### Anthropic API surface
110+
- [x] `ChatClientAgent` over the local shim round-trips through `AnthropicClient.Messages.GetClaudeMessageAsync` with multi-turn history preserved across the second call.
111+
112+
### Issues found / actions taken
113+
- **Implemented path 2 from the 2026-05-02 entry.** Added `samples/ch04-agent-framework/04.2.4-anthropic-agents/AnthropicChatClient.cs` -- a ~120-line `IChatClient` adapter that calls `AnthropicClient.Messages.GetClaudeMessageAsync` directly and translates between Anthropic's wire types and `Microsoft.Extensions.AI` types. Bypasses Anthropic.SDK's bundled `ChatClientHelper`, which is the path that touches the reshaped `HostedMcpServerTool.AuthorizationToken` and throws under M.E.AI 10.5. `Program.cs` now constructs `new AnthropicChatClient(new AnthropicClient(apiKey), modelId)` instead of `new AnthropicClient(apiKey).Messages`. README updated to explain the shim and to call out that it can be removed once Anthropic.SDK 5.11+ ships.
114+
- Streaming is implemented as a single-update wrapper around the non-streaming path; the chapter sample does not stream so this is sufficient. If streaming becomes part of the chapter prose, swap to `AnthropicClient.Messages.StreamClaudeMessageAsync` and yield per chunk.
115+
- Tool calling is intentionally not in the shim (the chapter sample is conversational only). Adding it would mean translating `ChatOptions.Tools` -> `MessageParameters.Tools` and translating `ToolUseContent` responses back into `FunctionCallContent`. Left as future work; not a print-blocker.
116+
117+
### Next-pass to-dos
118+
- [ ] Watch <https://www.nuget.org/packages/Anthropic.SDK> for 5.11+. When it ships rebuilt against M.E.AI 10.5+, delete `AnthropicChatClient.cs`, revert `Program.cs` to `new AnthropicClient(apiKey).Messages`, and re-run the live smoke.
119+
- [ ] Resume the broader URL audit deferred from 2026-05-02.
120+
121+
---
122+
97123
## 2026-05-02 -- P0-4 Anthropic live-API smoke
98124

99125
**Status:** Yellow -- model IDs and URLs verified green; one real defect (M.E.AI ↔ Anthropic.SDK runtime binding gap) blocks the existing Anthropic sample and must be cleared before print.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using Anthropic.SDK;
2+
using Anthropic.SDK.Messaging;
3+
using Microsoft.Extensions.AI;
4+
using AnthropicTextContent = Anthropic.SDK.Messaging.TextContent;
5+
using MEAITextContent = Microsoft.Extensions.AI.TextContent;
6+
7+
namespace Ch04.AnthropicAgent;
8+
9+
// Thin IChatClient adapter that calls AnthropicClient.Messages.GetClaudeMessageAsync directly,
10+
// bypassing Anthropic.SDK's bundled ChatClientHelper. The bundled helper was compiled against
11+
// Microsoft.Extensions.AI.Abstractions 10.3.0 and reads HostedMcpServerTool.AuthorizationToken,
12+
// which 10.5.0 reshaped -- so going through .Messages as IChatClient throws MissingMethodException
13+
// at runtime under the repo's central pin. See docs/verification-log.md (2026-05-02 entry) and
14+
// the README in this folder for the full chain.
15+
internal sealed class AnthropicChatClient : IChatClient
16+
{
17+
private const int DefaultMaxTokens = 1024;
18+
19+
private readonly AnthropicClient _client;
20+
private readonly string _defaultModel;
21+
private readonly int _defaultMaxTokens;
22+
23+
public AnthropicChatClient(AnthropicClient client, string defaultModel, int defaultMaxTokens = DefaultMaxTokens)
24+
{
25+
_client = client;
26+
_defaultModel = defaultModel;
27+
_defaultMaxTokens = defaultMaxTokens;
28+
}
29+
30+
public async Task<ChatResponse> GetResponseAsync(
31+
IEnumerable<ChatMessage> messages,
32+
ChatOptions? options = null,
33+
CancellationToken cancellationToken = default)
34+
{
35+
var (systemMessages, anthropicMessages) = Convert(messages);
36+
37+
var parameters = new MessageParameters
38+
{
39+
Model = options?.ModelId ?? _defaultModel,
40+
Messages = anthropicMessages,
41+
System = systemMessages.Count > 0 ? systemMessages : null,
42+
MaxTokens = options?.MaxOutputTokens ?? _defaultMaxTokens,
43+
Temperature = options?.Temperature is float t ? (decimal)t : null,
44+
TopP = options?.TopP is float p ? (decimal)p : null,
45+
StopSequences = options?.StopSequences?.ToArray(),
46+
Stream = false,
47+
};
48+
49+
MessageResponse response = await _client.Messages
50+
.GetClaudeMessageAsync(parameters, cancellationToken)
51+
.ConfigureAwait(false);
52+
53+
var text = string.Concat(response.Content.OfType<AnthropicTextContent>().Select(c => c.Text));
54+
55+
return new ChatResponse(new ChatMessage(ChatRole.Assistant, text))
56+
{
57+
ResponseId = response.Id,
58+
ModelId = response.Model,
59+
FinishReason = MapFinishReason(response.StopReason),
60+
};
61+
}
62+
63+
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
64+
IEnumerable<ChatMessage> messages,
65+
ChatOptions? options = null,
66+
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
67+
{
68+
// The chapter sample never streams. Adapt non-streaming to a single update so callers
69+
// that opportunistically choose streaming still get a usable response.
70+
var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
71+
yield return new ChatResponseUpdate(ChatRole.Assistant, response.Text)
72+
{
73+
ResponseId = response.ResponseId,
74+
ModelId = response.ModelId,
75+
FinishReason = response.FinishReason,
76+
};
77+
}
78+
79+
public object? GetService(Type serviceType, object? serviceKey = null)
80+
{
81+
ArgumentNullException.ThrowIfNull(serviceType);
82+
return serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null;
83+
}
84+
85+
public void Dispose()
86+
{
87+
// AnthropicClient lifetime is owned by the caller.
88+
}
89+
90+
private static (List<SystemMessage> System, List<Message> Messages) Convert(IEnumerable<ChatMessage> source)
91+
{
92+
var systemMessages = new List<SystemMessage>();
93+
var anthropicMessages = new List<Message>();
94+
95+
foreach (var message in source)
96+
{
97+
var text = ExtractText(message);
98+
if (string.IsNullOrEmpty(text))
99+
{
100+
continue;
101+
}
102+
103+
if (message.Role == ChatRole.System)
104+
{
105+
systemMessages.Add(new SystemMessage(text));
106+
continue;
107+
}
108+
109+
var role = message.Role == ChatRole.Assistant ? RoleType.Assistant : RoleType.User;
110+
anthropicMessages.Add(new Message
111+
{
112+
Role = role,
113+
Content = new List<ContentBase> { new AnthropicTextContent { Text = text } },
114+
});
115+
}
116+
117+
return (systemMessages, anthropicMessages);
118+
}
119+
120+
private static string ExtractText(ChatMessage message)
121+
{
122+
if (!string.IsNullOrEmpty(message.Text))
123+
{
124+
return message.Text;
125+
}
126+
127+
return string.Concat(message.Contents.OfType<MEAITextContent>().Select(c => c.Text));
128+
}
129+
130+
private static ChatFinishReason? MapFinishReason(string? stopReason) => stopReason switch
131+
{
132+
"end_turn" or "stop_sequence" => ChatFinishReason.Stop,
133+
"max_tokens" => ChatFinishReason.Length,
134+
"tool_use" => ChatFinishReason.ToolCalls,
135+
_ => null,
136+
};
137+
}

samples/ch04-agent-framework/04.2.4-anthropic-agents/Program.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Anthropic.SDK;
2+
using Ch04.AnthropicAgent;
23
using Microsoft.Agents.AI;
34
using Microsoft.Extensions.AI;
45

@@ -7,7 +8,10 @@
78

89
var modelId = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5-20251001";
910

10-
IChatClient chat = new AnthropicClient(apiKey).Messages;
11+
// AnthropicChatClient is a thin local shim -- see AnthropicChatClient.cs for the rationale.
12+
// Once Anthropic.SDK 5.11+ ships rebuilt against M.E.AI 10.5+, this can become
13+
// `new AnthropicClient(apiKey).Messages` again.
14+
IChatClient chat = new AnthropicChatClient(new AnthropicClient(apiKey), modelId);
1115

1216
ChatClientAgent agent = new(chat, new ChatClientAgentOptions
1317
{

samples/ch04-agent-framework/04.2.4-anthropic-agents/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
Companion code for **Generative AI in .NET**, Chapter 4 section 4.2.4 ("Anthropic Agents").
44

5-
Uses `new AnthropicClient(apiKey).Messages` -- which already implements `IChatClient` -- and hands it to a standard `ChatClientAgent`. The same agent API works against OpenAI, Azure OpenAI, Ollama, etc.
5+
Uses an `AnthropicClient` and hands its chat surface to a standard `ChatClientAgent`. The same agent API works against OpenAI, Azure OpenAI, Ollama, etc.
66

7-
> **Known issue (2026-05-02): this sample does not run as-shipped against the repo's central package pins.** It builds clean, but at runtime `ChatClientAgent.RunAsync(...)` throws `MissingMethodException: Method not found: 'System.String Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken()'.`. Root cause: `Anthropic.SDK` 5.10.0 was compiled against `Microsoft.Extensions.AI.Abstractions` 10.3.0; the repo pins 10.5.0 (required by `Microsoft.Agents.AI` 1.3.0), which reshapes that property. Per-project `VersionOverride` to 10.3.0 does not help here -- Agents.AI 1.3 itself rejects 10.3 with `CS1705` at compile time. **Mitigation paths under evaluation** (see [`docs/verification-log.md`](../../../docs/verification-log.md), entry `2026-05-02`): (1) wait for `Anthropic.SDK` 5.11+ rebuilt against 10.5+, (2) ship a thin custom `IChatClient` adapter that calls `AnthropicClient.Messages.GetClaudeMessageAsync` directly, or (3) re-target the sample at Anthropic's OpenAI-compatibility endpoint via the OpenAI SDK. The chapter prose still describes the intended pattern; once the underlying packages reconcile, the sample runs unchanged.
7+
> **Why the local `AnthropicChatClient.cs` shim?** `Anthropic.SDK` 5.10.0 was compiled against `Microsoft.Extensions.AI.Abstractions` 10.3.0, but the repo pins 10.5.0 (required by `Microsoft.Agents.AI` 1.3.0, which itself rejects 10.3 with `CS1705`). 10.5 reshaped `HostedMcpServerTool.AuthorizationToken`, so `new AnthropicClient(apiKey).Messages` (which routes through the SDK's bundled `ChatClientHelper`) throws `MissingMethodException` at runtime. The shim sidesteps the helper by calling `AnthropicClient.Messages.GetClaudeMessageAsync` directly and translating between Anthropic's wire types and `Microsoft.Extensions.AI` types itself. When `Anthropic.SDK` 5.11+ ships rebuilt against M.E.AI 10.5+, drop the shim and revert to `new AnthropicClient(apiKey).Messages`. See [`docs/verification-log.md`](../../../docs/verification-log.md) (entry `2026-05-02`) for the full chain of reasoning. The chapter prose describes the intended pattern; this folder ships a concrete adapter so the sample is runnable today.
88
99
## Run it
1010

0 commit comments

Comments
 (0)