Add novel-writing app: .NET 10 API, React front end, agent and MCP server
Builds out the vertical slice for planning and writing a novel. Three front ends — the React UI, an embedded Claude agent, and an MCP stdio server — all go through one REST API, so an edit made from Claude Code and one made in the browser are the same edit. Layout: Domain entities and enums, no dependencies Application services, DTOs, the agent tool-use loop and its 15 tools Infrastructure EF Core 10 + SQLite, Anthropic SDK client Api ASP.NET Core 10 minimal APIs, OpenAPI, ProblemDetails Mcp MCP stdio server, 21 tools over the same REST API Web React 19 + Vite + TanStack Query + Tailwind v4 Data model is Project > Characters / OutlineNodes / Chapters > Scenes, plus agent conversations. The outline is a self-nesting tree so acts, sequences and beats can be arranged however the book wants; scenes carry goal/conflict/outcome because that is what the agent drafts prose from. Notes on a few choices: - Conversation history replays to the model as text only. The agent re-reads current state through its tools rather than trusting a record of edits that may since have changed in the UI. - The user's turn is persisted before the tool loop runs, so a question is recorded even when the model call fails. Turn order uses an explicit sequence column; timestamps tie when a turn completes inside one tick. - Tool failures return is_error results rather than throwing, so the model can read the message and correct itself. MCP tools do the same via CallToolResult, which keeps the API's own message instead of a generic SDK error. - The Anthropic client is constructed lazily. It is injected into the agent service, which also serves read-only endpoints, and those should keep working on an install with no key. Sending without one returns 503, not 400. - DateTimeOffset is stored as UTC ticks. SQLite refuses to ORDER BY the default text form, which every "recently updated first" listing depends on. Tests run against real in-memory SQLite rather than the EF in-memory provider so they exercise the cascade deletes and query translation that actually ship. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c85bab4a4
commit
0d7b7a6f30
@@ -0,0 +1,42 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NovelSoftware.Application;
|
||||
using NovelSoftware.Application.Agent;
|
||||
using NovelSoftware.Infrastructure.Anthropic;
|
||||
|
||||
namespace NovelSoftware.Tests;
|
||||
|
||||
public class AnthropicClientTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructing_without_a_key_does_not_throw()
|
||||
{
|
||||
// The agent service takes this as a dependency and also serves read-only endpoints
|
||||
// (listing conversations, reading a transcript). Throwing at construction would
|
||||
// take those down on any install that has not configured a key yet.
|
||||
var construct = () => new AnthropicAgentModelClient(Options.Create(new AgentOptions()));
|
||||
|
||||
construct.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sending_without_a_key_reports_a_configuration_problem()
|
||||
{
|
||||
var previous = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
|
||||
Environment.SetEnvironmentVariable("ANTHROPIC_API_KEY", null);
|
||||
|
||||
try
|
||||
{
|
||||
var client = new AnthropicAgentModelClient(Options.Create(new AgentOptions()));
|
||||
|
||||
var send = async () => await client.CompleteAsync("system", [], []);
|
||||
|
||||
(await send.Should().ThrowAsync<AgentNotConfiguredException>())
|
||||
.WithMessage("*ANTHROPIC_API_KEY*");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ANTHROPIC_API_KEY", previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user