Replace chapter setting with multi-select locations; add chapter character summary

Chapters now carry many Locations (new Tags-style entity with cross-referencing)
instead of a single free-text Setting field, with a Locations tab on the novel
for browsing them and seeing every chapter set at each one. Also surfaces the
distinct characters appearing in a chapter's beats, linked, under the beat/word
count on the outline tab.
This commit is contained in:
James Wampler
2026-08-18 11:36:13 -07:00
parent 4313c8f206
commit c620ddd626
27 changed files with 2380 additions and 44 deletions
+26 -5
View File
@@ -3,6 +3,7 @@ using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Locations;
using Novelly.Api.Novels;
using Novelly.Api.Questions;
using Novelly.Api.Tags;
@@ -29,6 +30,7 @@ public class NovelAgentToolset(
ChapterService chapters,
BeatService beats,
TagService tags,
LocationService locations,
OpenQuestionService questions,
ILogger<NovelAgentToolset> logger)
{
@@ -366,6 +368,25 @@ public class NovelAgentToolset(
return await OrNotFound(tags.GetReferencesAsync(tagId, ct), t => t.ToReferencesResponse(), "Tag", tagId);
});
yield return new AgentTool(
"list_locations",
"List the novel's locations with how many chapters are set there. "
+ "Read this before inventing a new location so you reuse the writer's vocabulary.",
new JsonSchemaBuilder().Build(),
async (novelId, _, ct) => await locations.ListAsync(novelId, ct));
yield return new AgentTool(
"get_location_references",
"Cross-reference a location: every chapter set there.",
new JsonSchemaBuilder()
.Str("location_id", "Id of the location to trace.", required: true)
.Build(),
async (_, input, ct) =>
{
var locationId = JsonInput.RequiredGuid(input, "location_id");
return await OrNotFound(locations.GetReferencesAsync(locationId, ct), l => l.ToReferencesResponse(), "Location", locationId);
});
yield return new AgentTool(
"list_chapters",
"List the novel's chapters in manuscript order with beat and word counts.",
@@ -391,7 +412,7 @@ public class NovelAgentToolset(
.Str("title", "Chapter title.", required: true)
.Int("number", "Position in the manuscript, 1-based.")
.Str("summary", "What the chapter covers.")
.Str("setting", "Where and when the chapter takes place.")
.StringArray("locations", "Where and when the chapter takes place. Unknown locations are created.")
.Str("notes", "Anything else worth recording.")
.Enum("status", "Drafting status.", System.Enum.GetNames<DraftStatus>())
.Int("target_word_count", "Target length in words.")
@@ -402,7 +423,7 @@ public class NovelAgentToolset(
JsonInput.RequiredString(input, "title"),
JsonInput.Int(input, "number"),
JsonInput.String(input, "summary"),
JsonInput.String(input, "setting"),
JsonInput.Strings(input, "locations"),
JsonInput.String(input, "notes"),
JsonInput.Enum<DraftStatus>(input, "status") ?? DraftStatus.Planned,
JsonInput.Int(input, "target_word_count"),
@@ -411,7 +432,7 @@ public class NovelAgentToolset(
yield return new AgentTool(
"update_chapter",
"Revise a chapter's title, number, summary, setting, notes, status or drafted "
"Revise a chapter's title, number, summary, locations, notes, status or drafted "
+ "prose. Use 'prose' to write or replace the chapter's draft text in markdown; the "
+ "word count is recomputed automatically.",
new JsonSchemaBuilder()
@@ -419,7 +440,7 @@ public class NovelAgentToolset(
.Str("title", "New title.")
.Int("number", "Position in the manuscript.")
.Str("summary", "What the chapter covers.")
.Str("setting", "Where and when the chapter takes place.")
.StringArray("locations", "Where and when the chapter takes place. Replaces the existing locations. Unknown locations are created.")
.Str("notes", "Anything else worth recording.")
.Enum("status", "Drafting status.", System.Enum.GetNames<DraftStatus>())
.Int("target_word_count", "Target length in words.")
@@ -435,7 +456,7 @@ public class NovelAgentToolset(
JsonInput.String(input, "title"),
JsonInput.Int(input, "number"),
JsonInput.String(input, "summary"),
JsonInput.String(input, "setting"),
JsonInput.Strings(input, "locations"),
JsonInput.String(input, "notes"),
JsonInput.Enum<DraftStatus>(input, "status"),
JsonInput.Int(input, "target_word_count"),