Unify MCP and agent tool surfaces onto one registry in NovelAgentToolset
Fixes NovelAgentService continuing a conversation under the wrong novel's route, since FindConversationAsync matched by id alone. Then extends NovelAgentToolset to all 45 tools the stdio MCP server offered (tag/location CRUD, character relationships, arc-stage beat pinning, question editing, cross-novel novel listing/creation), tagging each with whether it needs an explicit novel scope so a later MCP adapter can inject it. Renames the toolset's 33 existing schemas from snake_case to camelCase to match .NET/REST convention, since nothing external consumes them. Lays the groundwork to serve this same registry over MCP at /mcp and retire the separate stdio Novelly.Mcp project (docs/plans/api/mcp_http_merge_plan.md).
This commit is contained in:
@@ -2,7 +2,7 @@ using System.Text.Json;
|
||||
|
||||
namespace Novelly.Api.Agent;
|
||||
|
||||
public record AgentToolDefinition(string Name, string Description, JsonElement InputSchema);
|
||||
public record AgentToolDefinition(string Name, string Description, JsonElement InputSchema, bool RequiresNovelId = false);
|
||||
|
||||
public abstract record AgentContentBlock;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ public class NovelAgentService(
|
||||
|
||||
logger.LogInformation("Getting agent conversation {ConversationId}", conversationId);
|
||||
|
||||
return await FindConversationAsync(conversationId, ct);
|
||||
return await FindConversationAsync(conversationId, null, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteConversationAsync(Guid conversationId, CancellationToken ct = default)
|
||||
@@ -52,7 +52,7 @@ public class NovelAgentService(
|
||||
|
||||
logger.LogInformation("Deleting agent conversation {ConversationId}", conversationId);
|
||||
|
||||
var conversation = await FindConversationAsync(conversationId, ct);
|
||||
var conversation = await FindConversationAsync(conversationId, null, ct);
|
||||
if (conversation is null)
|
||||
{
|
||||
return false;
|
||||
@@ -81,7 +81,7 @@ public class NovelAgentService(
|
||||
}
|
||||
|
||||
var conversation = request.ConversationId is { } id
|
||||
? await FindConversationAsync(id, ct)
|
||||
? await FindConversationAsync(id, novelId, ct)
|
||||
: StartConversation(novelId, request.Message);
|
||||
|
||||
if (conversation is null) return null;
|
||||
@@ -181,7 +181,7 @@ public class NovelAgentService(
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private async Task<AgentConversation?> FindConversationAsync(Guid conversationId, CancellationToken ct)
|
||||
private async Task<AgentConversation?> FindConversationAsync(Guid conversationId, Guid? novelId, CancellationToken ct)
|
||||
{
|
||||
logger.LogDebug("Finding agent conversation {ConversationId}", conversationId);
|
||||
|
||||
@@ -195,6 +195,14 @@ public class NovelAgentService(
|
||||
return conversation;
|
||||
}
|
||||
|
||||
if (novelId is { } expectedNovelId && conversation.NovelId != expectedNovelId)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"AgentConversation {ConversationId} belongs to novel {ActualNovelId}, not requested novel {NovelId}",
|
||||
conversationId, conversation.NovelId, expectedNovelId);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.LogDebug("Found agent conversation {ConversationId}", conversationId);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ public record AgentTool(
|
||||
string Name,
|
||||
string Description,
|
||||
JsonElement InputSchema,
|
||||
Func<Guid, JsonElement, CancellationToken, Task<object?>> Handler);
|
||||
Func<Guid, JsonElement, CancellationToken, Task<object?>> Handler,
|
||||
bool RequiresNovelId = false);
|
||||
|
||||
public class NovelAgentToolset(
|
||||
NovelService novels,
|
||||
@@ -46,7 +47,7 @@ public class NovelAgentToolset(
|
||||
private IReadOnlyList<AgentTool> Tools => [.. ByName.Values];
|
||||
|
||||
public IReadOnlyList<AgentToolDefinition> Definitions =>
|
||||
[.. Tools.Select(t => new AgentToolDefinition(t.Name, t.Description, t.InputSchema))];
|
||||
[.. Tools.Select(t => new AgentToolDefinition(t.Name, t.Description, t.InputSchema, t.RequiresNovelId))];
|
||||
|
||||
public async Task<AgentToolResult> ExecuteAsync(string name, Guid novelId, JsonElement input, CancellationToken ct = default)
|
||||
{
|
||||
@@ -97,12 +98,45 @@ public class NovelAgentToolset(
|
||||
|
||||
private IEnumerable<AgentTool> Build()
|
||||
{
|
||||
yield return new AgentTool(
|
||||
"list_novels",
|
||||
"List every novel, with counts of characters, chapters and drafted words. "
|
||||
+ "Start here to find the novel id everything else needs.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (_, _, ct) => await novels.ListAsync(ct));
|
||||
|
||||
yield return new AgentTool(
|
||||
"create_novel",
|
||||
"Create a new novel.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("title", "Working title.", required: true)
|
||||
.Str("author", "Author name.")
|
||||
.Str("genre", "Genre or category.")
|
||||
.Str("logline", "One-sentence pitch.")
|
||||
.Str("synopsis", "Paragraph-length summary of the whole book.")
|
||||
.Str("notes", "Free-form notes on theme, tone, comparable titles.")
|
||||
.Int("targetWordCount", "Target manuscript length in words.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var novel = await novels.CreateAsync(new CreateNovelRequest(
|
||||
JsonInput.RequiredString(input, "title"),
|
||||
JsonInput.String(input, "author"),
|
||||
JsonInput.String(input, "genre"),
|
||||
JsonInput.String(input, "logline"),
|
||||
JsonInput.String(input, "synopsis"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Int(input, "targetWordCount")), ct);
|
||||
return novel.ToResponse(null);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_novel_brief",
|
||||
"Read the novel's title, logline, synopsis, genre, notes and word-count target. "
|
||||
+ "Call this first in a conversation to ground yourself in what the book is.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (novelId, _, ct) => await OrNotFound(novels.GetAsync(novelId, ct), p => p.ToResponse(null), "Novel", novelId));
|
||||
async (novelId, _, ct) => await OrNotFound(novels.GetAsync(novelId, ct), p => p.ToResponse(null), "Novel", novelId),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_novel_brief",
|
||||
@@ -115,7 +149,7 @@ public class NovelAgentToolset(
|
||||
.Str("logline", "One-sentence pitch.")
|
||||
.Str("synopsis", "Paragraph-length summary of the whole book.")
|
||||
.Str("notes", "Free-form notes on theme, tone, comparable titles.")
|
||||
.Int("target_word_count", "Target manuscript length in words.")
|
||||
.Int("targetWordCount", "Target manuscript length in words.")
|
||||
.Build(),
|
||||
async (novelId, input, ct) => await OrNotFound(novels.UpdateAsync(novelId, new UpdateNovelRequest(
|
||||
JsonInput.String(input, "title"),
|
||||
@@ -124,13 +158,27 @@ public class NovelAgentToolset(
|
||||
JsonInput.String(input, "logline"),
|
||||
JsonInput.String(input, "synopsis"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Int(input, "target_word_count")), ct), p => p.ToResponse(null), "Novel", novelId));
|
||||
JsonInput.Int(input, "targetWordCount")), ct), p => p.ToResponse(null), "Novel", novelId),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_characters",
|
||||
"List every character in the novel with their full dossiers.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (novelId, _, ct) => (await characters.ListAsync(novelId, ct)).Select(c => c.ToResponse()));
|
||||
async (novelId, _, ct) => (await characters.ListAsync(novelId, ct)).Select(c => c.ToResponse()),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_character",
|
||||
"Read one character's dossier.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("characterId", "Id of the character to read.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
return await OrNotFound(characters.GetAsync(characterId, ct), c => c.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"create_character",
|
||||
@@ -152,17 +200,18 @@ public class NovelAgentToolset(
|
||||
JsonInput.String(input, "voice"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Strings(input, "tags"),
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Novel", novelId));
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Novel", novelId),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_character",
|
||||
"Revise an existing character dossier. Only the fields you supply change.",
|
||||
CharacterSchema(includeName: true, nameRequired: false)
|
||||
.Str("character_id", "Id of the character to update.", required: true)
|
||||
.Str("characterId", "Id of the character to update.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
return await OrNotFound(characters.UpdateAsync(
|
||||
characterId,
|
||||
new UpdateCharacterRequest(
|
||||
@@ -183,25 +232,49 @@ public class NovelAgentToolset(
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"relate_characters",
|
||||
"Record a relationship between two characters in the same novel. Creates both directions "
|
||||
+ "at once — characterId's side and relatedCharacterId's side — so the pair always shows up "
|
||||
+ "on both dossiers.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("characterId", "Id of the character the relationship belongs to.", required: true)
|
||||
.Str("relatedCharacterId", "Id of the character they are related to.", required: true)
|
||||
.Str("relationshipType", "How characterId is related to relatedCharacterId, e.g. 'sister', 'rival', 'former mentor'.", required: true)
|
||||
.Str("reciprocalRelationshipType", "How relatedCharacterId is related back to characterId, if different. Defaults to relationshipType when the relation is symmetric, like 'rival'.")
|
||||
.Str("description", "What the relationship is like, and where it is headed.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
return await OrNotFound(characters.AddRelationshipAsync(
|
||||
characterId,
|
||||
new CreateRelationshipRequest(
|
||||
JsonInput.RequiredGuid(input, "relatedCharacterId"),
|
||||
JsonInput.RequiredString(input, "relationshipType"),
|
||||
JsonInput.String(input, "description"),
|
||||
JsonInput.String(input, "reciprocalRelationshipType")), ct), c => c.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"link_character_identity",
|
||||
"Record that a character is really another character — e.g. one introduced under one name "
|
||||
+ "who is later revealed to be a character already in the novel under another name. Both "
|
||||
+ "keep their own dossier and beats; the canonical identity is whichever character you link to.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character being revealed as someone else.", required: true)
|
||||
.Str("same_character_as_id", "Id of the character this one really is.", required: true)
|
||||
.Str("revealed_in_chapter_id", "Id of the chapter where the reveal happens, if any.")
|
||||
.Str("characterId", "Id of the character being revealed as someone else.", required: true)
|
||||
.Str("sameCharacterAsId", "Id of the character this one really is.", required: true)
|
||||
.Str("revealedInChapterId", "Id of the chapter where the reveal happens, if any.")
|
||||
.Str("note", "Context on the reveal, e.g. how and why the disguise held.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
return await OrNotFound(characters.LinkIdentityAsync(
|
||||
characterId,
|
||||
new LinkCharacterIdentityRequest(
|
||||
JsonInput.RequiredGuid(input, "same_character_as_id"),
|
||||
JsonInput.Guid(input, "revealed_in_chapter_id"),
|
||||
JsonInput.RequiredGuid(input, "sameCharacterAsId"),
|
||||
JsonInput.Guid(input, "revealedInChapterId"),
|
||||
JsonInput.String(input, "note")), ct), c => c.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
@@ -209,11 +282,11 @@ public class NovelAgentToolset(
|
||||
"unlink_character_identity",
|
||||
"Remove a character's identity link, restoring it to its own separate identity.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character to unlink.", required: true)
|
||||
.Str("characterId", "Id of the character to unlink.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
return await DeletedOrNotFound(characters.UnlinkIdentityAsync(characterId, ct), "Character", characterId);
|
||||
});
|
||||
|
||||
@@ -222,29 +295,29 @@ public class NovelAgentToolset(
|
||||
"Read a chapter's outline: its summary paragraph and its beat table, in order. "
|
||||
+ "A beat is one row — a short title, whose beat it is, what happened, and what it sets up.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the chapter whose outline to read.", required: true)
|
||||
.Str("chapterId", "Id of the chapter whose outline to read.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) => (await beats.ListAsync(JsonInput.RequiredGuid(input, "chapter_id"), ct)).Select(b => b.ToResponse()));
|
||||
async (_, input, ct) => (await beats.ListAsync(JsonInput.RequiredGuid(input, "chapterId"), ct)).Select(b => b.ToResponse()));
|
||||
|
||||
yield return new AgentTool(
|
||||
"create_beat",
|
||||
"Add a beat to a chapter's outline. Keep the title to three to five words — it is a "
|
||||
+ "handle, not a sentence; the detail belongs in what_happened and whats_next.",
|
||||
+ "handle, not a sentence; the detail belongs in whatHappened and whatsNext.",
|
||||
BeatSchema()
|
||||
.Str("chapter_id", "Id of the chapter the beat belongs to.", required: true)
|
||||
.Str("chapterId", "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) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapterId");
|
||||
return await OrNotFound(beats.CreateAsync(
|
||||
chapterId,
|
||||
new CreateBeatRequest(
|
||||
JsonInput.RequiredString(input, "title"),
|
||||
JsonInput.Int(input, "sort_order"),
|
||||
JsonInput.Guids(input, "character_ids"),
|
||||
JsonInput.String(input, "what_happened"),
|
||||
JsonInput.String(input, "whats_next"),
|
||||
JsonInput.Int(input, "sortOrder"),
|
||||
JsonInput.Guids(input, "characterIds"),
|
||||
JsonInput.String(input, "whatHappened"),
|
||||
JsonInput.String(input, "whatsNext"),
|
||||
JsonInput.Strings(input, "tags")), ct), b => b.ToResponse(), "Chapter", chapterId);
|
||||
});
|
||||
|
||||
@@ -253,20 +326,20 @@ public class NovelAgentToolset(
|
||||
"Revise a beat. Only the fields you supply change. Supplying a tag list replaces "
|
||||
+ "the beat's tags outright, so include the ones you want to keep.",
|
||||
BeatSchema()
|
||||
.Str("beat_id", "Id of the beat to update.", required: true)
|
||||
.Str("beatId", "Id of the beat to update.", required: true)
|
||||
.Str("title", "Three to five words naming the beat.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var beatId = JsonInput.RequiredGuid(input, "beat_id");
|
||||
var beatId = JsonInput.RequiredGuid(input, "beatId");
|
||||
return await OrNotFound(beats.UpdateAsync(
|
||||
beatId,
|
||||
new UpdateBeatRequest(
|
||||
JsonInput.String(input, "title"),
|
||||
JsonInput.Int(input, "sort_order"),
|
||||
JsonInput.Guids(input, "character_ids"),
|
||||
JsonInput.String(input, "what_happened"),
|
||||
JsonInput.String(input, "whats_next"),
|
||||
JsonInput.Int(input, "sortOrder"),
|
||||
JsonInput.Guids(input, "characterIds"),
|
||||
JsonInput.String(input, "whatHappened"),
|
||||
JsonInput.String(input, "whatsNext"),
|
||||
JsonInput.Strings(input, "tags")), ct), b => b.ToResponse(), "Beat", beatId);
|
||||
});
|
||||
|
||||
@@ -274,11 +347,11 @@ public class NovelAgentToolset(
|
||||
"delete_beat",
|
||||
"Remove a beat from a chapter's outline. Confirm with the writer before calling it.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("beat_id", "Id of the beat to delete.", required: true)
|
||||
.Str("beatId", "Id of the beat to delete.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var beatId = JsonInput.RequiredGuid(input, "beat_id");
|
||||
var beatId = JsonInput.RequiredGuid(input, "beatId");
|
||||
return await DeletedOrNotFound(beats.DeleteAsync(beatId, ct), "Beat", beatId);
|
||||
});
|
||||
|
||||
@@ -287,16 +360,16 @@ public class NovelAgentToolset(
|
||||
"Renumber a chapter's beats to match the order given. List every beat id in the "
|
||||
+ "order you want; any you leave out keep their relative position at the end.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the chapter whose beats to reorder.", required: true)
|
||||
.StringArray("beat_ids", "Beat ids in their new order.", required: true)
|
||||
.Str("chapterId", "Id of the chapter whose beats to reorder.", required: true)
|
||||
.StringArray("beatIds", "Beat ids in their new order.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapterId");
|
||||
return await OrNotFound(beats.ReorderAsync(
|
||||
chapterId,
|
||||
new ReorderBeatsRequest(
|
||||
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
|
||||
[.. (JsonInput.Strings(input, "beatIds") ?? [])
|
||||
.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);
|
||||
});
|
||||
@@ -306,18 +379,18 @@ public class NovelAgentToolset(
|
||||
"Add a character to several beats at once. Leaves each beat's existing characters and "
|
||||
+ "other fields alone — this only adds, it never removes.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the chapter the beats belong to.", required: true)
|
||||
.Str("character_id", "Id of the character to add.", required: true)
|
||||
.StringArray("beat_ids", "Ids of the beats to add the character to.", required: true)
|
||||
.Str("chapterId", "Id of the chapter the beats belong to.", required: true)
|
||||
.Str("characterId", "Id of the character to add.", required: true)
|
||||
.StringArray("beatIds", "Ids of the beats to add the character to.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapterId");
|
||||
return await OrNotFound(beats.AssignCharacterAsync(
|
||||
chapterId,
|
||||
new AssignCharacterToBeatsRequest(
|
||||
JsonInput.RequiredGuid(input, "character_id"),
|
||||
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
|
||||
JsonInput.RequiredGuid(input, "characterId"),
|
||||
[.. (JsonInput.Strings(input, "beatIds") ?? [])
|
||||
.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);
|
||||
});
|
||||
@@ -327,18 +400,18 @@ public class NovelAgentToolset(
|
||||
"Move one or more beats from one chapter to another, appending them to the target "
|
||||
+ "chapter's end in the order given.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the beats' current chapter.", required: true)
|
||||
.Str("target_chapter_id", "Id of the chapter to move the beats into.", required: true)
|
||||
.StringArray("beat_ids", "Ids of the beats to move.", required: true)
|
||||
.Str("chapterId", "Id of the beats' current chapter.", required: true)
|
||||
.Str("targetChapterId", "Id of the chapter to move the beats into.", required: true)
|
||||
.StringArray("beatIds", "Ids of the beats to move.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapterId");
|
||||
return await OrNotFound(beats.MoveAsync(
|
||||
chapterId,
|
||||
new MoveBeatsRequest(
|
||||
JsonInput.RequiredGuid(input, "target_chapter_id"),
|
||||
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
|
||||
JsonInput.RequiredGuid(input, "targetChapterId"),
|
||||
[.. (JsonInput.Strings(input, "beatIds") ?? [])
|
||||
.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);
|
||||
});
|
||||
@@ -348,18 +421,19 @@ public class NovelAgentToolset(
|
||||
"List the novel's tags with how many characters, chapters and beats carry each. "
|
||||
+ "Read this before inventing a new tag so you reuse the writer's vocabulary.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (novelId, _, ct) => await tags.ListAsync(novelId, ct));
|
||||
async (novelId, _, ct) => await tags.ListAsync(novelId, ct),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_tag_references",
|
||||
"Cross-reference a tag: every character, chapter and beat carrying it. Use this to "
|
||||
+ "trace a motif or a thread through the book.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("tag_id", "Id of the tag to trace.", required: true)
|
||||
.Str("tagId", "Id of the tag to trace.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var tagId = JsonInput.RequiredGuid(input, "tag_id");
|
||||
var tagId = JsonInput.RequiredGuid(input, "tagId");
|
||||
var tag = await tags.GetReferencesAsync(tagId, ct);
|
||||
if (tag is null)
|
||||
{
|
||||
@@ -370,22 +444,68 @@ public class NovelAgentToolset(
|
||||
return tag.ToReferencesResponse(displayNumbers);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"create_tag",
|
||||
"Create a tag explicitly. Applying an unknown tag by name to a character, chapter or "
|
||||
+ "beat also creates it, so this is only needed to set a colour up front.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("name", "The tag's name. Unique within the novel, matched case-insensitively.", required: true)
|
||||
.Str("color", "Optional hex colour for the UI, e.g. \"#9a4a2f\".")
|
||||
.Build(),
|
||||
async (novelId, input, ct) => await OrNotFound(tags.CreateAsync(
|
||||
novelId,
|
||||
new CreateTagRequest(
|
||||
JsonInput.RequiredString(input, "name"),
|
||||
JsonInput.String(input, "color")), ct), t => t.ToResponse(), "Novel", novelId),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_tag",
|
||||
"Rename or recolour a tag. Renaming updates it everywhere it is applied.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("tagId", "Id of the tag to update.", required: true)
|
||||
.Str("name", "New name.")
|
||||
.Str("color", "Hex colour, e.g. \"#9a4a2f\".")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var tagId = JsonInput.RequiredGuid(input, "tagId");
|
||||
return await OrNotFound(tags.UpdateAsync(
|
||||
tagId,
|
||||
new UpdateTagRequest(
|
||||
JsonInput.String(input, "name"),
|
||||
JsonInput.String(input, "color")), ct), t => t.ToResponse(), "Tag", tagId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"delete_tag",
|
||||
"Delete a tag. Whatever carried it is left alone — only the label goes.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("tagId", "Id of the tag to delete.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var tagId = JsonInput.RequiredGuid(input, "tagId");
|
||||
return await DeletedOrNotFound(tags.DeleteAsync(tagId, ct), "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));
|
||||
async (novelId, _, ct) => await locations.ListAsync(novelId, ct),
|
||||
RequiresNovelId: true);
|
||||
|
||||
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)
|
||||
.Str("locationId", "Id of the location to trace.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var locationId = JsonInput.RequiredGuid(input, "location_id");
|
||||
var locationId = JsonInput.RequiredGuid(input, "locationId");
|
||||
var location = await locations.GetReferencesAsync(locationId, ct);
|
||||
if (location is null)
|
||||
{
|
||||
@@ -396,6 +516,45 @@ public class NovelAgentToolset(
|
||||
return location.ToReferencesResponse(displayNumbers);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"create_location",
|
||||
"Create a location explicitly. Applying an unknown location by name to a chapter also "
|
||||
+ "creates it, so this is only needed to set one up ahead of time.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("name", "The location's name. Unique within the novel, matched case-insensitively.", required: true)
|
||||
.Build(),
|
||||
async (novelId, input, ct) => await OrNotFound(locations.CreateAsync(
|
||||
novelId,
|
||||
new CreateLocationRequest(JsonInput.RequiredString(input, "name")), ct), l => l.ToResponse(), "Novel", novelId),
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_location",
|
||||
"Rename a location. Renaming updates it everywhere it is applied.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("locationId", "Id of the location to update.", required: true)
|
||||
.Str("name", "New name.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var locationId = JsonInput.RequiredGuid(input, "locationId");
|
||||
return await OrNotFound(locations.UpdateAsync(
|
||||
locationId,
|
||||
new UpdateLocationRequest(JsonInput.RequiredString(input, "name")), ct), l => l.ToResponse(), "Location", locationId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"delete_location",
|
||||
"Delete a location. Whatever carried it is left alone — only the label goes.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("locationId", "Id of the location to delete.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var locationId = JsonInput.RequiredGuid(input, "locationId");
|
||||
return await DeletedOrNotFound(locations.DeleteAsync(locationId, ct), "Location", locationId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_chapters",
|
||||
"List the novel's chapters in manuscript order with beat and word counts.",
|
||||
@@ -405,17 +564,18 @@ public class NovelAgentToolset(
|
||||
var list = await chapters.ListAsync(novelId, ct);
|
||||
var displayNumbers = ChapterNumbering.DisplayNumbers(list);
|
||||
return list.Select(c => c.ToSummaryResponse(displayNumbers.TryGetValue(c.Id, out var n) ? n : null));
|
||||
});
|
||||
},
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_chapter",
|
||||
"Read one chapter in full: its outline (beats) and its drafted prose.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the chapter to read.", required: true)
|
||||
.Str("chapterId", "Id of the chapter to read.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapterId");
|
||||
var chapter = await chapters.GetAsync(chapterId, ct);
|
||||
if (chapter is null)
|
||||
{
|
||||
@@ -439,7 +599,7 @@ public class NovelAgentToolset(
|
||||
.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.")
|
||||
.Int("targetWordCount", "Target length in words.")
|
||||
.Str("prose", "The chapter's drafted text, in markdown, if you are writing it now.")
|
||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
|
||||
.Build(),
|
||||
@@ -453,7 +613,7 @@ public class NovelAgentToolset(
|
||||
JsonInput.Strings(input, "locations"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Enum<DraftStatus>(input, "status") ?? DraftStatus.Planned,
|
||||
JsonInput.Int(input, "target_word_count"),
|
||||
JsonInput.Int(input, "targetWordCount"),
|
||||
JsonInput.String(input, "prose"),
|
||||
JsonInput.Strings(input, "tags")), ct);
|
||||
|
||||
@@ -464,7 +624,8 @@ public class NovelAgentToolset(
|
||||
|
||||
var displayNumber = await chapters.DisplayNumberAsync(chapter, ct);
|
||||
return chapter.ToResponse(displayNumber);
|
||||
});
|
||||
},
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_chapter",
|
||||
@@ -472,7 +633,7 @@ public class NovelAgentToolset(
|
||||
+ "prose. Use 'prose' to write or replace the chapter's draft text in markdown; the "
|
||||
+ "word count is recomputed automatically.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the chapter to update.", required: true)
|
||||
.Str("chapterId", "Id of the chapter to update.", required: true)
|
||||
.Str("title", "New title.")
|
||||
.Int("number", "Manuscript position, 1-based, counting front and back matter.")
|
||||
.Enum("kind", "Front matter, a numbered body chapter, or back matter.", System.Enum.GetNames<ChapterKind>())
|
||||
@@ -480,13 +641,13 @@ public class NovelAgentToolset(
|
||||
.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.")
|
||||
.Int("targetWordCount", "Target length in words.")
|
||||
.Str("prose", "The chapter's drafted text, in markdown.")
|
||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapterId");
|
||||
var chapter = await chapters.UpdateAsync(
|
||||
chapterId,
|
||||
new UpdateChapterRequest(
|
||||
@@ -497,7 +658,7 @@ public class NovelAgentToolset(
|
||||
JsonInput.Strings(input, "locations"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Enum<DraftStatus>(input, "status"),
|
||||
JsonInput.Int(input, "target_word_count"),
|
||||
JsonInput.Int(input, "targetWordCount"),
|
||||
JsonInput.String(input, "prose"),
|
||||
JsonInput.Strings(input, "tags")), ct);
|
||||
|
||||
@@ -516,11 +677,11 @@ public class NovelAgentToolset(
|
||||
+ "Read this before revising a character — it is what they actually do on the page, "
|
||||
+ "as opposed to what the dossier claims about them.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character.", required: true)
|
||||
.Str("characterId", "Id of the character.", required: true)
|
||||
.Build(),
|
||||
async (novelId, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
var characterBeats = await beats.ListForCharacterAsync(characterId, ct);
|
||||
if (characterBeats is null)
|
||||
{
|
||||
@@ -530,66 +691,67 @@ public class NovelAgentToolset(
|
||||
var displayNumbers = await chapterLabels.ForNovelAsync(novelId, ct);
|
||||
return characterBeats.Select(b =>
|
||||
b.ToCharacterBeatResponse(characterId, b.Chapter is null ? null : chapterLabels.LabelFor(b.Chapter, displayNumbers)));
|
||||
});
|
||||
},
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_character_arc",
|
||||
"Read a main character's arc: the ordered stages of how they change. Each stage may "
|
||||
+ "be pinned to the chapter where it lands.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character.", required: true)
|
||||
.Str("characterId", "Id of the character.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) => (await arcs.ListAsync(
|
||||
JsonInput.RequiredGuid(input, "character_id"), ct)).Select(s => s.ToResponse()));
|
||||
JsonInput.RequiredGuid(input, "characterId"), ct)).Select(s => s.ToResponse()));
|
||||
|
||||
yield return new AgentTool(
|
||||
"add_arc_stage",
|
||||
"Add a stage to a character's arc. Arcs are for main characters — promote the "
|
||||
+ "character first with update_character if they are still Supporting.",
|
||||
ArcStageSchema()
|
||||
.Str("character_id", "Id of the character whose arc to add to.", required: true)
|
||||
.Str("characterId", "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) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
return await OrNotFound(arcs.CreateAsync(
|
||||
characterId,
|
||||
new CreateArcStageRequest(
|
||||
JsonInput.RequiredString(input, "title"),
|
||||
JsonInput.Int(input, "sort_order"),
|
||||
JsonInput.Int(input, "sortOrder"),
|
||||
JsonInput.String(input, "description"),
|
||||
JsonInput.Guid(input, "chapter_id")), ct), s => s.ToResponse(), "Character", characterId);
|
||||
JsonInput.Guid(input, "chapterId")), ct), s => s.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_arc_stage",
|
||||
"Revise a stage of a character's arc. Only the fields you supply change.",
|
||||
ArcStageSchema()
|
||||
.Str("arc_stage_id", "Id of the arc stage to update.", required: true)
|
||||
.Str("arcStageId", "Id of the arc stage to update.", required: true)
|
||||
.Str("title", "New title for the stage.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var arcStageId = JsonInput.RequiredGuid(input, "arc_stage_id");
|
||||
var arcStageId = JsonInput.RequiredGuid(input, "arcStageId");
|
||||
return await OrNotFound(arcs.UpdateAsync(
|
||||
arcStageId,
|
||||
new UpdateArcStageRequest(
|
||||
JsonInput.String(input, "title"),
|
||||
JsonInput.Int(input, "sort_order"),
|
||||
JsonInput.Int(input, "sortOrder"),
|
||||
JsonInput.String(input, "description"),
|
||||
JsonInput.Guid(input, "chapter_id")), ct), s => s.ToResponse(), "CharacterArcStage", arcStageId);
|
||||
JsonInput.Guid(input, "chapterId")), ct), s => s.ToResponse(), "CharacterArcStage", arcStageId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"delete_arc_stage",
|
||||
"Remove a stage from a character's arc.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("arc_stage_id", "Id of the arc stage to delete.", required: true)
|
||||
.Str("arcStageId", "Id of the arc stage to delete.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var arcStageId = JsonInput.RequiredGuid(input, "arc_stage_id");
|
||||
var arcStageId = JsonInput.RequiredGuid(input, "arcStageId");
|
||||
return await DeletedOrNotFound(arcs.DeleteAsync(arcStageId, ct), "CharacterArcStage", arcStageId);
|
||||
});
|
||||
|
||||
@@ -598,16 +760,35 @@ public class NovelAgentToolset(
|
||||
"Renumber a character's arc to match the order given. Stages left out keep their "
|
||||
+ "relative position after the ones listed.",
|
||||
new JsonSchemaBuilder()
|
||||
.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)
|
||||
.Str("characterId", "Id of the character whose arc to reorder.", required: true)
|
||||
.StringArray("stageIds", "Arc stage ids in the order wanted.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
var characterId = JsonInput.RequiredGuid(input, "characterId");
|
||||
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);
|
||||
[.. JsonInput.Strings(input, "stageIds")?.Select(Guid.Parse) ?? []]), ct), list => list.Select(s => s.ToResponse()), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"set_arc_stage_beats",
|
||||
"Set which beats belong to an arc stage, replacing its current set. This groups the "
|
||||
+ "chapter-level beats that establish or pay off this stage of the character's arc. A "
|
||||
+ "beat moved into this stage leaves any other stage of the same character it was in. "
|
||||
+ "Each beat must already include this character.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("arcStageId", "Id of the arc stage.", required: true)
|
||||
.StringArray("beatIds", "Beat ids that belong to this stage, replacing whatever was there before.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var arcStageId = JsonInput.RequiredGuid(input, "arcStageId");
|
||||
return await OrNotFound(arcs.SetBeatsAsync(
|
||||
arcStageId,
|
||||
new SetArcStageBeatsRequest(
|
||||
[.. JsonInput.Strings(input, "beatIds")?.Select(Guid.Parse) ?? []]), ct), s => s.ToResponse(), "CharacterArcStage", arcStageId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
@@ -615,22 +796,23 @@ public class NovelAgentToolset(
|
||||
"The decisions the writer has not made yet. Read this before proposing changes — an "
|
||||
+ "open question is a place the writer is still thinking, not a gap to fill in for them.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Narrow to questions about one chapter outline.")
|
||||
.Str("character_id", "Narrow to questions about one character.")
|
||||
.Bool("include_resolved", "Include questions already settled. Defaults to false.")
|
||||
.Str("chapterId", "Narrow to questions about one chapter outline.")
|
||||
.Str("characterId", "Narrow to questions about one character.")
|
||||
.Bool("includeResolved", "Include questions already settled. Defaults to false.")
|
||||
.Build(),
|
||||
async (novelId, input, ct) =>
|
||||
{
|
||||
var list = await questions.ListAsync(
|
||||
novelId,
|
||||
JsonInput.Guid(input, "chapter_id"),
|
||||
JsonInput.Guid(input, "character_id"),
|
||||
JsonInput.Bool(input, "include_resolved") ?? false,
|
||||
JsonInput.Guid(input, "chapterId"),
|
||||
JsonInput.Guid(input, "characterId"),
|
||||
JsonInput.Bool(input, "includeResolved") ?? false,
|
||||
ct);
|
||||
|
||||
var displayNumbers = await chapterLabels.ForNovelAsync(novelId, ct);
|
||||
return list.Select(q => q.ToResponse(displayNumbers));
|
||||
});
|
||||
},
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"raise_open_question",
|
||||
@@ -640,8 +822,8 @@ public class NovelAgentToolset(
|
||||
new JsonSchemaBuilder()
|
||||
.Str("question", "The question, in one line.", required: true)
|
||||
.Str("detail", "The thinking around it — options, and what each costs.")
|
||||
.Str("chapter_id", "The chapter outline this is about, if any.")
|
||||
.Str("character_id", "The character this is about, if any.")
|
||||
.Str("chapterId", "The chapter outline this is about, if any.")
|
||||
.Str("characterId", "The character this is about, if any.")
|
||||
.Build(),
|
||||
async (novelId, input, ct) =>
|
||||
{
|
||||
@@ -650,8 +832,8 @@ public class NovelAgentToolset(
|
||||
new CreateOpenQuestionRequest(
|
||||
JsonInput.RequiredString(input, "question"),
|
||||
JsonInput.String(input, "detail"),
|
||||
JsonInput.Guid(input, "chapter_id"),
|
||||
JsonInput.Guid(input, "character_id")), ct);
|
||||
JsonInput.Guid(input, "chapterId"),
|
||||
JsonInput.Guid(input, "characterId")), ct);
|
||||
|
||||
if (question is null)
|
||||
{
|
||||
@@ -660,25 +842,60 @@ public class NovelAgentToolset(
|
||||
|
||||
var displayNumbers = await chapterLabels.ForNovelAsync(novelId, ct);
|
||||
return question.ToResponse(displayNumbers);
|
||||
},
|
||||
RequiresNovelId: true);
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_open_question",
|
||||
"Revise a question or change what it is attached to. Only the fields you supply change.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("questionId", "Id of the question to update.", required: true)
|
||||
.Str("question", "New wording for the question.")
|
||||
.Str("detail", "New detail. Pass an empty string to clear it.")
|
||||
.Str("chapterId", "Attach to this chapter outline.")
|
||||
.Str("characterId", "Attach to this character.")
|
||||
.Bool("clearChapter", "Detach from its chapter.")
|
||||
.Bool("clearCharacter", "Detach from its character.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var questionId = JsonInput.RequiredGuid(input, "questionId");
|
||||
var question = await questions.UpdateAsync(
|
||||
questionId,
|
||||
new UpdateOpenQuestionRequest(
|
||||
JsonInput.String(input, "question"),
|
||||
JsonInput.String(input, "detail"),
|
||||
JsonInput.Guid(input, "chapterId"),
|
||||
JsonInput.Guid(input, "characterId"),
|
||||
JsonInput.Bool(input, "clearChapter") ?? false,
|
||||
JsonInput.Bool(input, "clearCharacter") ?? false), ct);
|
||||
|
||||
if (question is null)
|
||||
{
|
||||
return new ToolNotFound("OpenQuestion", questionId);
|
||||
}
|
||||
|
||||
var displayNumbers = await chapterLabels.ForNovelAsync(question.NovelId, ct);
|
||||
return question.ToResponse(displayNumbers);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"resolve_open_question",
|
||||
"Settle a question with what the writer decided. Set append_to_notes to also write "
|
||||
"Settle a question with what the writer decided. Set appendToNotes to also write "
|
||||
+ "the resolution into the notes of the chapter and character it hangs off.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("question_id", "Id of the question to resolve.", required: true)
|
||||
.Str("questionId", "Id of the question to resolve.", required: true)
|
||||
.Str("resolution", "What was decided.", required: true)
|
||||
.Bool("append_to_notes", "Also append the resolution to the associated notes.")
|
||||
.Bool("appendToNotes", "Also append the resolution to the associated notes.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var questionId = JsonInput.RequiredGuid(input, "question_id");
|
||||
var questionId = JsonInput.RequiredGuid(input, "questionId");
|
||||
var question = await questions.ResolveAsync(
|
||||
questionId,
|
||||
new ResolveOpenQuestionRequest(
|
||||
JsonInput.RequiredString(input, "resolution"),
|
||||
JsonInput.Bool(input, "append_to_notes") ?? false), ct);
|
||||
JsonInput.Bool(input, "appendToNotes") ?? false), ct);
|
||||
|
||||
if (question is null)
|
||||
{
|
||||
@@ -693,11 +910,11 @@ public class NovelAgentToolset(
|
||||
"reopen_question",
|
||||
"Put a resolved question back on the list. Anything already appended to notes stays.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("question_id", "Id of the question to reopen.", required: true)
|
||||
.Str("questionId", "Id of the question to reopen.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var questionId = JsonInput.RequiredGuid(input, "question_id");
|
||||
var questionId = JsonInput.RequiredGuid(input, "questionId");
|
||||
var question = await questions.ReopenAsync(questionId, ct);
|
||||
if (question is null)
|
||||
{
|
||||
@@ -712,20 +929,20 @@ public class NovelAgentToolset(
|
||||
"delete_open_question",
|
||||
"Delete a question outright. Resolving is usually better — it keeps the decision.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("question_id", "Id of the question to delete.", required: true)
|
||||
.Str("questionId", "Id of the question to delete.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var questionId = JsonInput.RequiredGuid(input, "question_id");
|
||||
var questionId = JsonInput.RequiredGuid(input, "questionId");
|
||||
return await DeletedOrNotFound(questions.DeleteAsync(questionId, ct), "OpenQuestion", questionId);
|
||||
});
|
||||
}
|
||||
|
||||
private static JsonSchemaBuilder ArcStageSchema() =>
|
||||
new JsonSchemaBuilder()
|
||||
.Int("sort_order", "Position in the arc. Appended to the end when omitted.")
|
||||
.Int("sortOrder", "Position in the arc. Appended to the end when omitted.")
|
||||
.Str("description", "What shifts in the character here, and what it costs them.")
|
||||
.Str("chapter_id", "The chapter where this stage lands, if it is pinned to one.");
|
||||
.Str("chapterId", "The chapter where this stage lands, if it is pinned to one.");
|
||||
|
||||
private static JsonSchemaBuilder CharacterSchema(bool includeName, bool nameRequired)
|
||||
{
|
||||
@@ -759,9 +976,9 @@ public class NovelAgentToolset(
|
||||
|
||||
private static JsonSchemaBuilder BeatSchema() =>
|
||||
new JsonSchemaBuilder()
|
||||
.Int("sort_order", "Position in the chapter. Appended to the end when omitted.")
|
||||
.StringArray("character_ids", "Ids of the characters whose beat this is. Replaces the existing list.")
|
||||
.Str("what_happened", "The event itself.")
|
||||
.Str("whats_next", "What it sets in motion — the hook into the next beat.")
|
||||
.Int("sortOrder", "Position in the chapter. Appended to the end when omitted.")
|
||||
.StringArray("characterIds", "Ids of the characters whose beat this is. Replaces the existing list.")
|
||||
.Str("whatHappened", "The event itself.")
|
||||
.Str("whatsNext", "What it sets in motion — the hook into the next beat.")
|
||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user