Remove NotFoundException; services return null on lookup miss

A missing record isn't exceptional — services now return null (logged
at Info) instead of throwing, and endpoints map null to 404. Agent and
import toolsets route not-found through their existing OrNotFound
result pattern rather than a caught exception.
This commit is contained in:
James Wampler
2026-08-06 21:22:40 -07:00
parent 189ebf3237
commit 2ccebb31eb
22 changed files with 305 additions and 212 deletions
+66 -51
View File
@@ -83,11 +83,6 @@ public class NovelAgentToolset(
logger.LogDebug("Tool {Tool} for project {ProjectId} succeeded", name, projectId);
return new AgentToolResult(JsonSerializer.Serialize(result, SerializerOptions), false);
}
catch (NotFoundException ex)
{
logger.LogWarning(ex, "Tool {Tool} for project {ProjectId} failed: not found", name, projectId);
return new AgentToolResult(ex.Message, true);
}
catch (ArgumentException ex)
{
logger.LogWarning(ex, "Tool {Tool} for project {ProjectId} failed: invalid argument", name, projectId);
@@ -157,7 +152,7 @@ public class NovelAgentToolset(
"Add a character dossier. Name is the only requirement — leave fields blank when "
+ "the writer has not decided them yet rather than inventing detail.",
CharacterSchema(includeName: true, nameRequired: true).Build(),
async (projectId, input, ct) => (await characters.CreateAsync(projectId, new CreateCharacterRequest(
async (projectId, input, ct) => await OrNotFound(characters.CreateAsync(projectId, new CreateCharacterRequest(
JsonInput.RequiredString(input, "name"),
JsonInput.Enum<CharacterRole>(input, "role") ?? CharacterRole.Supporting,
JsonInput.Enum<CharacterImportance>(input, "importance") ?? CharacterImportance.Supporting,
@@ -174,7 +169,7 @@ public class NovelAgentToolset(
JsonInput.String(input, "arc_summary"),
JsonInput.String(input, "voice"),
JsonInput.String(input, "notes"),
JsonInput.Strings(input, "tags")), ct)).ToResponse());
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Project", projectId));
yield return new AgentTool(
"update_character",
@@ -224,16 +219,20 @@ public class NovelAgentToolset(
.Str("chapter_id", "Id of the chapter the beat belongs to.", required: true)
.Str("title", "Three to five words naming the beat.", required: true)
.Build(),
async (_, input, ct) => (await beats.CreateAsync(
JsonInput.RequiredGuid(input, "chapter_id"),
new CreateBeatRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "sort_order"),
JsonInput.Guid(input, "character_id"),
JsonInput.String(input, "what_happened"),
JsonInput.String(input, "whats_next"),
JsonInput.Guid(input, "scene_id"),
JsonInput.Strings(input, "tags")), ct)).ToResponse());
async (_, input, ct) =>
{
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
return await OrNotFound(beats.CreateAsync(
chapterId,
new CreateBeatRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "sort_order"),
JsonInput.Guid(input, "character_id"),
JsonInput.String(input, "what_happened"),
JsonInput.String(input, "whats_next"),
JsonInput.Guid(input, "scene_id"),
JsonInput.Strings(input, "tags")), ct), b => b.ToResponse(), "Chapter", chapterId);
});
yield return new AgentTool(
"update_beat",
@@ -278,12 +277,16 @@ public class NovelAgentToolset(
.Str("chapter_id", "Id of the chapter whose beats to reorder.", required: true)
.StringArray("beat_ids", "Beat ids in their new order.", required: true)
.Build(),
async (_, input, ct) => (await beats.ReorderAsync(
JsonInput.RequiredGuid(input, "chapter_id"),
new ReorderBeatsRequest(
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
.Select(id => Guid.TryParse(id, out var g) ? g : Guid.Empty)
.Where(g => g != Guid.Empty)]), ct)).Select(b => b.ToResponse()));
async (_, input, ct) =>
{
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
return await OrNotFound(beats.ReorderAsync(
chapterId,
new ReorderBeatsRequest(
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
.Select(id => Guid.TryParse(id, out var g) ? g : Guid.Empty)
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
});
yield return new AgentTool(
"list_tags",
@@ -337,7 +340,7 @@ public class NovelAgentToolset(
.Int("target_word_count", "Target length in words.")
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
.Build(),
async (projectId, input, ct) => (await chapters.CreateAsync(projectId, new CreateChapterRequest(
async (projectId, input, ct) => await OrNotFound(chapters.CreateAsync(projectId, new CreateChapterRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "number"),
JsonInput.String(input, "summary"),
@@ -346,7 +349,7 @@ public class NovelAgentToolset(
JsonInput.String(input, "notes"),
JsonInput.Enum<DraftStatus>(input, "status") ?? DraftStatus.Planned,
JsonInput.Int(input, "target_word_count"),
JsonInput.Strings(input, "tags")), ct)).ToResponse());
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Project", projectId));
yield return new AgentTool(
"update_chapter",
@@ -388,19 +391,23 @@ public class NovelAgentToolset(
.Str("chapter_id", "Id of the chapter the scene belongs to.", required: true)
.Str("title", "Scene title.", required: true)
.Build(),
async (_, input, ct) => (await scenes.CreateAsync(
JsonInput.RequiredGuid(input, "chapter_id"),
new CreateSceneRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "sort_order"),
JsonInput.String(input, "summary"),
JsonInput.String(input, "goal"),
JsonInput.String(input, "conflict"),
JsonInput.String(input, "outcome"),
JsonInput.Guid(input, "pov_character_id"),
JsonInput.String(input, "location"),
JsonInput.String(input, "prose"),
JsonInput.Enum<DraftStatus>(input, "status") ?? DraftStatus.Planned), ct)).ToResponse());
async (_, input, ct) =>
{
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
return await OrNotFound(scenes.CreateAsync(
chapterId,
new CreateSceneRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "sort_order"),
JsonInput.String(input, "summary"),
JsonInput.String(input, "goal"),
JsonInput.String(input, "conflict"),
JsonInput.String(input, "outcome"),
JsonInput.Guid(input, "pov_character_id"),
JsonInput.String(input, "location"),
JsonInput.String(input, "prose"),
JsonInput.Enum<DraftStatus>(input, "status") ?? DraftStatus.Planned), ct), s => s.ToResponse(), "Chapter", chapterId);
});
yield return new AgentTool(
"update_scene",
@@ -464,13 +471,17 @@ public class NovelAgentToolset(
.Str("character_id", "Id of the character whose arc to add to.", required: true)
.Str("title", "A short handle for the change, three to five words.", required: true)
.Build(),
async (_, input, ct) => (await arcs.CreateAsync(
JsonInput.RequiredGuid(input, "character_id"),
new CreateArcStageRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "sort_order"),
JsonInput.String(input, "description"),
JsonInput.Guid(input, "chapter_id")), ct)).ToResponse());
async (_, input, ct) =>
{
var characterId = JsonInput.RequiredGuid(input, "character_id");
return await OrNotFound(arcs.CreateAsync(
characterId,
new CreateArcStageRequest(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "sort_order"),
JsonInput.String(input, "description"),
JsonInput.Guid(input, "chapter_id")), ct), s => s.ToResponse(), "Character", characterId);
});
yield return new AgentTool(
"update_arc_stage",
@@ -511,10 +522,14 @@ public class NovelAgentToolset(
.Str("character_id", "Id of the character whose arc to reorder.", required: true)
.StringArray("stage_ids", "Arc stage ids in the order wanted.", required: true)
.Build(),
async (_, input, ct) => (await arcs.ReorderAsync(
JsonInput.RequiredGuid(input, "character_id"),
new ReorderArcStagesRequest(
[.. JsonInput.Strings(input, "stage_ids")?.Select(Guid.Parse) ?? []]), ct)).Select(s => s.ToResponse()));
async (_, input, ct) =>
{
var characterId = JsonInput.RequiredGuid(input, "character_id");
return await OrNotFound(arcs.ReorderAsync(
characterId,
new ReorderArcStagesRequest(
[.. JsonInput.Strings(input, "stage_ids")?.Select(Guid.Parse) ?? []]), ct), list => list.Select(s => s.ToResponse()), "Character", characterId);
});
yield return new AgentTool(
"list_open_questions",
@@ -543,13 +558,13 @@ public class NovelAgentToolset(
.Str("chapter_id", "The chapter outline this is about, if any.")
.Str("character_id", "The character this is about, if any.")
.Build(),
async (projectId, input, ct) => (await questions.CreateAsync(
async (projectId, input, ct) => await OrNotFound(questions.CreateAsync(
projectId,
new CreateOpenQuestionRequest(
JsonInput.RequiredString(input, "question"),
JsonInput.String(input, "detail"),
JsonInput.Guid(input, "chapter_id"),
JsonInput.Guid(input, "character_id")), ct)).ToResponse());
JsonInput.Guid(input, "character_id")), ct), q => q.ToResponse(), "Project", projectId));
yield return new AgentTool(
"resolve_open_question",