perf: 上下文分级裁剪与Agent完成性保障
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/voocel/agentcore"
|
"github.com/voocel/agentcore"
|
||||||
|
"github.com/voocel/agentcore/memory"
|
||||||
"github.com/voocel/ainovel-cli/state"
|
"github.com/voocel/ainovel-cli/state"
|
||||||
"github.com/voocel/ainovel-cli/tools"
|
"github.com/voocel/ainovel-cli/tools"
|
||||||
)
|
)
|
||||||
@@ -80,12 +81,19 @@ func BuildCoordinator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
writer := agentcore.SubAgentConfig{
|
writer := agentcore.SubAgentConfig{
|
||||||
Name: "writer",
|
Name: "writer",
|
||||||
Description: "创作者:自主完成一章的构思、写作、自审和提交",
|
Description: "创作者:自主完成一章的构思、写作、自审和提交",
|
||||||
Model: model,
|
Model: model,
|
||||||
SystemPrompt: writerPrompt,
|
SystemPrompt: writerPrompt,
|
||||||
Tools: writerTools,
|
Tools: writerTools,
|
||||||
MaxTurns: 20,
|
MaxTurns: 20,
|
||||||
|
TransformContext: memory.NewCompaction(memory.CompactionConfig{
|
||||||
|
Model: model,
|
||||||
|
ContextWindow: cfg.ContextWindow,
|
||||||
|
ReserveTokens: 16384,
|
||||||
|
KeepRecentTokens: 20000,
|
||||||
|
}),
|
||||||
|
ConvertToLLM: memory.CompactionConvertToLLM,
|
||||||
}
|
}
|
||||||
|
|
||||||
editor := agentcore.SubAgentConfig{
|
editor := agentcore.SubAgentConfig{
|
||||||
@@ -104,6 +112,15 @@ func BuildCoordinator(
|
|||||||
agentcore.WithSystemPrompt(prompts.Coordinator),
|
agentcore.WithSystemPrompt(prompts.Coordinator),
|
||||||
agentcore.WithTools(subagentTool, contextTool, askUser),
|
agentcore.WithTools(subagentTool, contextTool, askUser),
|
||||||
agentcore.WithMaxTurns(60),
|
agentcore.WithMaxTurns(60),
|
||||||
|
agentcore.WithContextPipeline(
|
||||||
|
memory.NewCompaction(memory.CompactionConfig{
|
||||||
|
Model: model,
|
||||||
|
ContextWindow: cfg.ContextWindow,
|
||||||
|
ReserveTokens: 32000,
|
||||||
|
KeepRecentTokens: 30000,
|
||||||
|
}),
|
||||||
|
memory.CompactionConvertToLLM,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return agent, askUser
|
return agent, askUser
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import (
|
|||||||
|
|
||||||
// Config 小说应用配置。
|
// Config 小说应用配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Prompt string // 用户的小说需求
|
Prompt string // 用户的小说需求
|
||||||
NovelName string // 小说名(用作输出目录名)
|
NovelName string // 小说名(用作输出目录名)
|
||||||
OutputDir string // 输出根目录,默认 output/{NovelName}
|
OutputDir string // 输出根目录,默认 output/{NovelName}
|
||||||
Provider string // LLM 提供商:openai / anthropic / gemini
|
Provider string // LLM 提供商:openai / anthropic / gemini
|
||||||
ModelName string // LLM 模型名
|
ModelName string // LLM 模型名
|
||||||
APIKey string // API Key
|
APIKey string // API Key
|
||||||
BaseURL string // API Base URL(可选)
|
BaseURL string // API Base URL(可选)
|
||||||
Style string // 写作风格(default/suspense/fantasy/romance)
|
Style string // 写作风格(default/suspense/fantasy/romance)
|
||||||
|
ContextWindow int // 模型上下文窗口大小(token),默认 128000
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompts 嵌入的提示词。
|
// Prompts 嵌入的提示词。
|
||||||
@@ -72,4 +73,7 @@ func (c *Config) FillDefaults() {
|
|||||||
if c.Style == "" {
|
if c.Style == "" {
|
||||||
c.Style = "default"
|
c.Style = "default"
|
||||||
}
|
}
|
||||||
|
if c.ContextWindow <= 0 {
|
||||||
|
c.ContextWindow = 128000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
181
app/run.go
181
app/run.go
@@ -182,21 +182,40 @@ func registerSubscription(coordinator *agentcore.Agent, store *state.Store, prov
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subagent 结果:提取 usage 和 error,单独记录
|
||||||
|
if ev.Tool == "subagent" {
|
||||||
|
logSubAgentResult(ev.Result, emit)
|
||||||
|
handleFoundationCheck(coordinator, store, emit)
|
||||||
|
committed := handleSubAgentDone(coordinator, store, emit)
|
||||||
|
if !committed {
|
||||||
|
handleUncommittedDraft(coordinator, store, emit)
|
||||||
|
}
|
||||||
|
handleEditorDone(coordinator, store, emit)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// novel_context:提取加载摘要替代原始 JSON
|
||||||
|
if ev.Tool == "novel_context" {
|
||||||
|
if summary := extractLoadingSummary(ev.Result); summary != "" {
|
||||||
|
log.Printf("[tool:done] novel_context → %s", summary)
|
||||||
|
if emit != nil {
|
||||||
|
emit(UIEvent{Time: time.Now(), Category: "CONTEXT", Summary: summary, Level: "info"})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[tool:done] novel_context → %s", truncateLog(string(ev.Result), 200))
|
||||||
|
}
|
||||||
|
if emit != nil {
|
||||||
|
emit(UIEvent{Time: time.Now(), Category: "TOOL", Summary: "novel_context.done", Level: "info"})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他工具:保持原样
|
||||||
log.Printf("[tool:done] %s → %s", ev.Tool, truncateLog(string(ev.Result), 200))
|
log.Printf("[tool:done] %s → %s", ev.Tool, truncateLog(string(ev.Result), 200))
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(UIEvent{Time: time.Now(), Category: "TOOL", Summary: ev.Tool + ".done", Level: "info"})
|
emit(UIEvent{Time: time.Now(), Category: "TOOL", Summary: ev.Tool + ".done", Level: "info"})
|
||||||
}
|
}
|
||||||
// 上下文加载可视化:提取 novel_context 的加载摘要
|
|
||||||
if ev.Tool == "novel_context" && emit != nil {
|
|
||||||
if summary := extractLoadingSummary(ev.Result); summary != "" {
|
|
||||||
emit(UIEvent{Time: time.Now(), Category: "CONTEXT", Summary: summary, Level: "info"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ev.Tool == "subagent" {
|
|
||||||
handleSubAgentDone(coordinator, store, emit)
|
|
||||||
handleEditorDone(coordinator, store, emit)
|
|
||||||
}
|
|
||||||
|
|
||||||
case agentcore.EventMessageEnd:
|
case agentcore.EventMessageEnd:
|
||||||
if ev.Message != nil && ev.Message.GetRole() == agentcore.RoleAssistant {
|
if ev.Message != nil && ev.Message.GetRole() == agentcore.RoleAssistant {
|
||||||
@@ -337,11 +356,52 @@ func determineRecovery(progress *domain.Progress, runMeta *domain.RunMeta) recov
|
|||||||
return recoveryResult{IsNew: true}
|
return recoveryResult{IsNew: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleFoundationCheck 在 SubAgent 完成后检查基础设定是否完备。
|
||||||
|
// 如果 phase 仍在 premise(有 premise 但无 outline),注入确定性提醒。
|
||||||
|
func handleFoundationCheck(coordinator *agentcore.Agent, store *state.Store, emit emitFn) {
|
||||||
|
progress, _ := store.LoadProgress()
|
||||||
|
if progress == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 只在规划阶段检查(premise 已保存但 outline 未保存)
|
||||||
|
if progress.Phase != domain.PhasePremise {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var missing []string
|
||||||
|
if o, _ := store.LoadOutline(); len(o) == 0 {
|
||||||
|
missing = append(missing, "outline")
|
||||||
|
}
|
||||||
|
if c, _ := store.LoadCharacters(); len(c) == 0 {
|
||||||
|
missing = append(missing, "characters")
|
||||||
|
}
|
||||||
|
if r, _ := store.LoadWorldRules(); len(r) == 0 {
|
||||||
|
missing = append(missing, "world_rules")
|
||||||
|
}
|
||||||
|
if len(missing) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[host] 基础设定不完整,缺失: %v", missing)
|
||||||
|
if emit != nil {
|
||||||
|
emit(UIEvent{Time: time.Now(), Category: "SYSTEM",
|
||||||
|
Summary: fmt.Sprintf("基础设定不完整,缺失: %v", missing), Level: "warn"})
|
||||||
|
}
|
||||||
|
runMeta, _ := store.LoadRunMeta()
|
||||||
|
guidance := planningTierGuidance(runMeta)
|
||||||
|
msg := fmt.Sprintf(
|
||||||
|
"[系统] 基础设定不完整,以下项目尚未保存:%v。请重新调用对应规划师补全这些设定。在基础设定全部完备前,不要调用 writer。",
|
||||||
|
missing)
|
||||||
|
if guidance != "" {
|
||||||
|
msg += "\n" + guidance
|
||||||
|
}
|
||||||
|
coordinator.FollowUp(agentcore.UserMsg(msg))
|
||||||
|
}
|
||||||
|
|
||||||
// handleSubAgentDone 在每次 SubAgent 调用完成后读取文件系统信号,注入确定性任务。
|
// handleSubAgentDone 在每次 SubAgent 调用完成后读取文件系统信号,注入确定性任务。
|
||||||
func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit emitFn) {
|
// 返回 true 表示检测到 commit 信号(Writer 正常完成)。
|
||||||
|
func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit emitFn) bool {
|
||||||
result, err := store.LoadLastCommit()
|
result, err := store.LoadLastCommit()
|
||||||
if err != nil || result == nil {
|
if err != nil || result == nil {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
if err := store.ClearLastCommit(); err != nil {
|
if err := store.ClearLastCommit(); err != nil {
|
||||||
log.Printf("[host] 清除 commit 信号失败: %v", err)
|
log.Printf("[host] 清除 commit 信号失败: %v", err)
|
||||||
@@ -378,7 +438,7 @@ func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit e
|
|||||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||||
"[系统] 当前处于重写流程,但提交了非队列章节(第 %d 章)。请先完成待重写章节 %v 后再继续新章节。",
|
"[系统] 当前处于重写流程,但提交了非队列章节(第 %d 章)。请先完成待重写章节 %v 后再继续新章节。",
|
||||||
result.Chapter, progress.PendingRewrites)))
|
result.Chapter, progress.PendingRewrites)))
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
if err := store.CompleteRewrite(result.Chapter); err != nil {
|
if err := store.CompleteRewrite(result.Chapter); err != nil {
|
||||||
log.Printf("[host] 完成重写标记失败: %v", err)
|
log.Printf("[host] 完成重写标记失败: %v", err)
|
||||||
@@ -396,7 +456,7 @@ func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit e
|
|||||||
log.Printf("[host] 还有 %d 章待处理:%v", len(updated.PendingRewrites), updated.PendingRewrites)
|
log.Printf("[host] 还有 %d 章待处理:%v", len(updated.PendingRewrites), updated.PendingRewrites)
|
||||||
saveCheckpoint(store, fmt.Sprintf("ch%02d-commit", result.Chapter))
|
saveCheckpoint(store, fmt.Sprintf("ch%02d-commit", result.Chapter))
|
||||||
}
|
}
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定性判断 1.5:长篇弧/卷边界处理
|
// 确定性判断 1.5:长篇弧/卷边界处理
|
||||||
@@ -454,7 +514,7 @@ func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit e
|
|||||||
}
|
}
|
||||||
clearHandledSteer(store)
|
clearHandledSteer(store)
|
||||||
saveCheckpoint(store, fmt.Sprintf("ch%02d-commit", result.Chapter))
|
saveCheckpoint(store, fmt.Sprintf("ch%02d-commit", result.Chapter))
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定性判断 1:全书完成(TotalChapters 由大纲自动设定)
|
// 确定性判断 1:全书完成(TotalChapters 由大纲自动设定)
|
||||||
@@ -475,7 +535,7 @@ func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit e
|
|||||||
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||||
"[系统] 全部 %d 章已写完。请总结全书并结束。不要再调用 writer。",
|
"[系统] 全部 %d 章已写完。请总结全书并结束。不要再调用 writer。",
|
||||||
totalChapters)))
|
totalChapters)))
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定性判断 2:需要全局审阅
|
// 确定性判断 2:需要全局审阅
|
||||||
@@ -493,6 +553,37 @@ func handleSubAgentDone(coordinator *agentcore.Agent, store *state.Store, emit e
|
|||||||
}
|
}
|
||||||
clearHandledSteer(store)
|
clearHandledSteer(store)
|
||||||
saveCheckpoint(store, fmt.Sprintf("ch%02d-commit", result.Chapter))
|
saveCheckpoint(store, fmt.Sprintf("ch%02d-commit", result.Chapter))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUncommittedDraft 在 Writer 结束但没有 commit 时检测是否存在未提交的草稿。
|
||||||
|
// 如果存在,提醒 Coordinator 重新调用 writer 完成提交。
|
||||||
|
func handleUncommittedDraft(coordinator *agentcore.Agent, store *state.Store, emit emitFn) {
|
||||||
|
progress, _ := store.LoadProgress()
|
||||||
|
if progress == nil || progress.Phase == domain.PhaseComplete {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 确定下一个应该写的章节
|
||||||
|
next := 1
|
||||||
|
if progress.InProgressChapter > 0 {
|
||||||
|
next = progress.InProgressChapter
|
||||||
|
} else if len(progress.CompletedChapters) > 0 {
|
||||||
|
next = progress.NextChapter()
|
||||||
|
}
|
||||||
|
// 检查该章节是否有草稿但未提交
|
||||||
|
draft, _ := store.LoadDraft(next)
|
||||||
|
if draft == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 有草稿但没有 commit 信号
|
||||||
|
log.Printf("[host] Writer 结束但第 %d 章草稿未提交", next)
|
||||||
|
if emit != nil {
|
||||||
|
emit(UIEvent{Time: time.Now(), Category: "SYSTEM",
|
||||||
|
Summary: fmt.Sprintf("第 %d 章有草稿但未提交", next), Level: "warn"})
|
||||||
|
}
|
||||||
|
coordinator.FollowUp(agentcore.UserMsg(fmt.Sprintf(
|
||||||
|
"[系统] Writer 结束但第 %d 章草稿未提交。请重新调用 writer 完成该章的自审和提交(commit_chapter)。",
|
||||||
|
next)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleEditorDone 在 Editor SubAgent 完成后读取审阅信号。
|
// handleEditorDone 在 Editor SubAgent 完成后读取审阅信号。
|
||||||
@@ -599,10 +690,11 @@ func parseProgressSummary(ev agentcore.Event) string {
|
|||||||
return "progress"
|
return "progress"
|
||||||
}
|
}
|
||||||
var data struct {
|
var data struct {
|
||||||
Agent string `json:"agent"`
|
Agent string `json:"agent"`
|
||||||
Tool string `json:"tool"`
|
Tool string `json:"tool"`
|
||||||
Turn int `json:"turn"`
|
Turn int `json:"turn"`
|
||||||
Error bool `json:"error"`
|
Error bool `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
Thinking string `json:"thinking"`
|
Thinking string `json:"thinking"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(ev.Result, &data); err != nil {
|
if err := json.Unmarshal(ev.Result, &data); err != nil {
|
||||||
@@ -614,6 +706,9 @@ func parseProgressSummary(ev agentcore.Event) string {
|
|||||||
}
|
}
|
||||||
if data.Tool != "" {
|
if data.Tool != "" {
|
||||||
if data.Error {
|
if data.Error {
|
||||||
|
if data.Message != "" {
|
||||||
|
return fmt.Sprintf("%s → %s (error: %s)", data.Agent, data.Tool, truncateLog(data.Message, 120))
|
||||||
|
}
|
||||||
return fmt.Sprintf("%s → %s (error)", data.Agent, data.Tool)
|
return fmt.Sprintf("%s → %s (error)", data.Agent, data.Tool)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s → %s", data.Agent, data.Tool)
|
return fmt.Sprintf("%s → %s", data.Agent, data.Tool)
|
||||||
@@ -638,6 +733,50 @@ func extractLoadingSummary(result json.RawMessage) string {
|
|||||||
return data.Summary
|
return data.Summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logSubAgentResult 从 subagent 结果中提取 usage 和 error,分别记录结构化日志。
|
||||||
|
func logSubAgentResult(result json.RawMessage, emit emitFn) {
|
||||||
|
if len(result) == 0 {
|
||||||
|
log.Printf("[tool:done] subagent → (empty)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var data struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Usage struct {
|
||||||
|
Input int `json:"input"`
|
||||||
|
Output int `json:"output"`
|
||||||
|
CacheRead int `json:"cache_read"`
|
||||||
|
CacheWrite int `json:"cache_write"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
Turns int `json:"turns"`
|
||||||
|
Tools int `json:"tools"`
|
||||||
|
} `json:"usage"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(result, &data); err != nil {
|
||||||
|
log.Printf("[tool:done] subagent → %s", truncateLog(string(result), 200))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录 usage
|
||||||
|
u := data.Usage
|
||||||
|
log.Printf("[usage] input=%d output=%d cache_read=%d turns=%d tools=%d",
|
||||||
|
u.Input, u.Output, u.CacheRead, u.Turns, u.Tools)
|
||||||
|
|
||||||
|
if data.Error != "" {
|
||||||
|
log.Printf("[subagent:error] %s", data.Error)
|
||||||
|
if emit != nil {
|
||||||
|
emit(UIEvent{Time: time.Now(), Category: "ERROR",
|
||||||
|
Summary: "subagent: " + truncateLog(data.Error, 80), Level: "error"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[tool:done] subagent → %s", truncateLog(data.Output, 200))
|
||||||
|
if emit != nil {
|
||||||
|
emit(UIEvent{Time: time.Now(), Category: "TOOL", Summary: "subagent.done", Level: "info"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func extractToolErrorText(result json.RawMessage) string {
|
func extractToolErrorText(result json.RawMessage) string {
|
||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ func (p *Progress) NextChapter() int {
|
|||||||
type ContextProfile struct {
|
type ContextProfile struct {
|
||||||
SummaryWindow int // 加载最近 N 章摘要
|
SummaryWindow int // 加载最近 N 章摘要
|
||||||
TimelineWindow int // 加载最近 N 章时间线
|
TimelineWindow int // 加载最近 N 章时间线
|
||||||
FullContext bool // true = 忽略窗口,全量加载
|
|
||||||
Layered bool // true = 启用分层摘要加载(卷摘要+弧摘要+章摘要)
|
Layered bool // true = 启用分层摘要加载(卷摘要+弧摘要+章摘要)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +83,11 @@ type ContextProfile struct {
|
|||||||
func NewContextProfile(totalChapters int) ContextProfile {
|
func NewContextProfile(totalChapters int) ContextProfile {
|
||||||
switch {
|
switch {
|
||||||
case totalChapters <= 15:
|
case totalChapters <= 15:
|
||||||
return ContextProfile{FullContext: true}
|
return ContextProfile{SummaryWindow: 10, TimelineWindow: 10}
|
||||||
case totalChapters <= 50:
|
case totalChapters <= 50:
|
||||||
return ContextProfile{SummaryWindow: 5, TimelineWindow: 10}
|
return ContextProfile{SummaryWindow: 5, TimelineWindow: 8}
|
||||||
default:
|
default:
|
||||||
return ContextProfile{SummaryWindow: 3, TimelineWindow: 8, Layered: true}
|
return ContextProfile{SummaryWindow: 3, TimelineWindow: 5, Layered: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -6,7 +6,8 @@ require (
|
|||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/voocel/agentcore v1.5.1
|
github.com/voocel/agentcore v1.5.3
|
||||||
|
github.com/voocel/litellm v1.6.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -28,8 +29,9 @@ require (
|
|||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/voocel/litellm v1.6.0 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.3.8 // indirect
|
golang.org/x/text v0.3.8 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/voocel/agentcore => ../agentcore
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -44,10 +44,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
|||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/voocel/agentcore v1.5.1 h1:gEVpBXZfXH4fkq4fLISo2dYfoQ+SaJ0NsetU/Y0hKrI=
|
github.com/voocel/litellm v1.6.2 h1:TJ1s7B7UqgV86O1EcuwQTZua0FK1tbOg0+oUsDmgmuA=
|
||||||
github.com/voocel/agentcore v1.5.1/go.mod h1:fjksENApgfL1QXbcJY8RUUU5Gl03YOYExFAZ040X/zU=
|
github.com/voocel/litellm v1.6.2/go.mod h1:6MBUu3I4DHm7h72Vl+3nqLruSwYmgqMf/I9BGoordJ4=
|
||||||
github.com/voocel/litellm v1.6.0 h1:jc0Y7q+cp6QQcag3Mhmd6wMKkfzf7mXjXY0Uvj5VBQw=
|
|
||||||
github.com/voocel/litellm v1.6.0/go.mod h1:6MBUu3I4DHm7h72Vl+3nqLruSwYmgqMf/I9BGoordJ4=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
|||||||
@@ -114,3 +114,4 @@
|
|||||||
- 不要过早透支所有高潮和谜底
|
- 不要过早透支所有高潮和谜底
|
||||||
- 不要把同一种爽点反复复制到每一卷
|
- 不要把同一种爽点反复复制到每一卷
|
||||||
- 不要让中后期只是前期的放大版
|
- 不要让中后期只是前期的放大版
|
||||||
|
- **你必须按顺序完成全部 4 步(premise → layered_outline → characters → world_rules),全部保存后才算完成。每次 save_foundation 返回值中的 `remaining` 字段会告诉你还有哪些未完成,不要在 remaining 非空时停止。**
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
- 中篇的关键是阶段推进和平衡
|
- 中篇的关键是阶段推进和平衡
|
||||||
- 不要像短篇那样过度压缩
|
- 不要像短篇那样过度压缩
|
||||||
- 也不要像长篇那样预留过多远期空间
|
- 也不要像长篇那样预留过多远期空间
|
||||||
|
- **你必须按顺序完成全部 4 步(premise → outline → characters → world_rules),全部保存后才算完成。每次 save_foundation 返回值中的 `remaining` 字段会告诉你还有哪些未完成,不要在 remaining 非空时停止。**
|
||||||
|
|||||||
@@ -106,4 +106,5 @@
|
|||||||
|
|
||||||
- 短篇最重要的是集中与收束
|
- 短篇最重要的是集中与收束
|
||||||
- 不要预埋大量未来再说的线
|
- 不要预埋大量未来再说的线
|
||||||
- 不要把短篇写成“长篇开头”
|
- 不要把短篇写成”长篇开头”
|
||||||
|
- **你必须按顺序完成全部 4 步(premise → outline → characters → world_rules),全部保存后才算完成。每次 save_foundation 返回值中的 `remaining` 字段会告诉你还有哪些未完成,不要在 remaining 非空时停止。**
|
||||||
|
|||||||
@@ -60,18 +60,18 @@
|
|||||||
调用对应规划师完成基础设定:
|
调用对应规划师完成基础设定:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"agent": "architect_short", "task": "根据以下需求生成短篇/单卷小说基础设定。\\n\\n<用户需求>"}
|
{"agent": "architect_short", "task": "根据以下需求生成短篇/单卷小说基础设定(premise + outline + characters + world_rules,全部保存后才算完成)。\\n\\n<用户需求>"}
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"agent": "architect_mid", "task": "根据以下需求生成中篇/多阶段小说基础设定。\\n\\n<用户需求>"}
|
{"agent": "architect_mid", "task": "根据以下需求生成中篇/多阶段小说基础设定(premise + outline + characters + world_rules,全部保存后才算完成)。\\n\\n<用户需求>"}
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"agent": "architect_long", "task": "根据以下需求生成长篇/连载型小说基础设定。\\n\\n<用户需求>"}
|
{"agent": "architect_long", "task": "根据以下需求生成长篇/连载型小说基础设定(premise + layered_outline + characters + world_rules,全部保存后才算完成)。\\n\\n<用户需求>"}
|
||||||
```
|
```
|
||||||
|
|
||||||
规划完成后,用 novel_context 确认设定已保存,再开始写作。
|
规划完成后,用 novel_context(不传 chapter)确认设定已保存。**必须检查返回值中的 `foundation_status.ready` 为 true 且 `foundation_status.missing` 为空**。如果有缺失项,重新调用对应规划师补全缺失部分,不要跳过直接写作。
|
||||||
|
|
||||||
### 第二阶段:逐章写作
|
### 第二阶段:逐章写作
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@
|
|||||||
如果写作过程中发现某个角色比预期更有魅力、某条支线比主线更有趣、或大纲的走向不太对,你可以在 commit_chapter 的 feedback 字段中反馈。系统会将你的建议转达给 Coordinator 评估。
|
如果写作过程中发现某个角色比预期更有魅力、某条支线比主线更有趣、或大纲的走向不太对,你可以在 commit_chapter 的 feedback 字段中反馈。系统会将你的建议转达给 Coordinator 评估。
|
||||||
|
|
||||||
## 提交要求
|
## 提交要求
|
||||||
|
**你必须在完成写作后调用 commit_chapter,这是你的核心职责。没有 commit 就等于没有完成任何工作。** draft_chapter 只是保存草稿,commit_chapter 才是正式提交。
|
||||||
|
|
||||||
commit_chapter 时提供:
|
commit_chapter 时提供:
|
||||||
- summary: 本章内容摘要(200字以内)
|
- summary: 本章内容摘要(200字以内)
|
||||||
- characters: 本章出场角色名列表(使用正式名)
|
- characters: 本章出场角色名列表(使用正式名)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func NewCheckConsistencyTool(store *state.Store) *CheckConsistencyTool {
|
|||||||
|
|
||||||
func (t *CheckConsistencyTool) Name() string { return "check_consistency" }
|
func (t *CheckConsistencyTool) Name() string { return "check_consistency" }
|
||||||
func (t *CheckConsistencyTool) Description() string {
|
func (t *CheckConsistencyTool) Description() string {
|
||||||
return "加载章节内容和全部状态数据(时间线、伏笔、关系、世界规则、角色状态),供你自行对照检查一致性"
|
return "加载章节内容和对照数据(世界规则、伏笔、关系、别名、最近摘要),供你检查一致性"
|
||||||
}
|
}
|
||||||
func (t *CheckConsistencyTool) Label() string { return "一致性检查" }
|
func (t *CheckConsistencyTool) Label() string { return "一致性检查" }
|
||||||
|
|
||||||
@@ -55,18 +55,17 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
|
|||||||
result["content"] = content
|
result["content"] = content
|
||||||
result["word_count"] = wordCount
|
result["word_count"] = wordCount
|
||||||
|
|
||||||
// 状态数据(全部加载,Agent 自行决定怎么用)
|
// 对照数据:保留全局性的一致性检查数据,避免重复加载 novel_context 已有的窗口数据
|
||||||
if timeline, _ := t.store.LoadTimeline(); len(timeline) > 0 {
|
if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
|
||||||
result["timeline"] = timeline
|
result["world_rules"] = rules
|
||||||
}
|
}
|
||||||
if foreshadow, _ := t.store.LoadForeshadowLedger(); len(foreshadow) > 0 {
|
if foreshadow, _ := t.store.LoadActiveForeshadow(); len(foreshadow) > 0 {
|
||||||
result["foreshadow_ledger"] = foreshadow
|
result["foreshadow_ledger"] = foreshadow
|
||||||
}
|
}
|
||||||
if relationships, _ := t.store.LoadRelationships(); len(relationships) > 0 {
|
if relationships, _ := t.store.LoadRelationships(); len(relationships) > 0 {
|
||||||
result["relationships"] = relationships
|
result["relationships"] = relationships
|
||||||
}
|
}
|
||||||
if chars, _ := t.store.LoadCharacters(); len(chars) > 0 {
|
if chars, _ := t.store.LoadCharacters(); len(chars) > 0 {
|
||||||
result["characters"] = chars
|
|
||||||
aliasMap := make(map[string]string)
|
aliasMap := make(map[string]string)
|
||||||
for _, c := range chars {
|
for _, c := range chars {
|
||||||
for _, alias := range c.Aliases {
|
for _, alias := range c.Aliases {
|
||||||
@@ -77,12 +76,6 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
|
|||||||
result["alias_map"] = aliasMap
|
result["alias_map"] = aliasMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changes, _ := t.store.LoadRecentStateChanges(a.Chapter, 5); len(changes) > 0 {
|
|
||||||
result["recent_state_changes"] = changes
|
|
||||||
}
|
|
||||||
if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
|
|
||||||
result["world_rules"] = rules
|
|
||||||
}
|
|
||||||
if summaries, _ := t.store.LoadRecentSummaries(a.Chapter, 2); len(summaries) > 0 {
|
if summaries, _ := t.store.LoadRecentSummaries(a.Chapter, 2); len(summaries) > 0 {
|
||||||
result["recent_summaries"] = summaries
|
result["recent_summaries"] = summaries
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func (t *DraftChapterTool) Execute(_ context.Context, args json.RawMessage) (jso
|
|||||||
"chapter": a.Chapter,
|
"chapter": a.Chapter,
|
||||||
"mode": "append",
|
"mode": "append",
|
||||||
"word_count": utf8.RuneCountInString(full),
|
"word_count": utf8.RuneCountInString(full),
|
||||||
|
"next_step": "自审后调用 commit_chapter 提交",
|
||||||
})
|
})
|
||||||
default: // write
|
default: // write
|
||||||
if err := t.store.SaveDraft(a.Chapter, a.Content); err != nil {
|
if err := t.store.SaveDraft(a.Chapter, a.Content); err != nil {
|
||||||
@@ -75,6 +76,7 @@ func (t *DraftChapterTool) Execute(_ context.Context, args json.RawMessage) (jso
|
|||||||
"chapter": a.Chapter,
|
"chapter": a.Chapter,
|
||||||
"mode": "write",
|
"mode": "write",
|
||||||
"word_count": utf8.RuneCountInString(a.Content),
|
"word_count": utf8.RuneCountInString(a.Content),
|
||||||
|
"next_step": "自审后调用 commit_chapter 提交",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,15 +126,9 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
|||||||
warn("current_chapter_outline", err)
|
warn("current_chapter_outline", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 摘要加载:分层 vs 扁平
|
// 摘要加载:分层 vs 扁平窗口
|
||||||
if profile.Layered {
|
if profile.Layered {
|
||||||
t.loadLayeredSummaries(result, a.Chapter, profile.SummaryWindow, warn)
|
t.loadLayeredSummaries(result, a.Chapter, profile.SummaryWindow, warn)
|
||||||
} else if profile.FullContext {
|
|
||||||
if summaries, err := t.store.LoadAllSummaries(a.Chapter); err == nil && len(summaries) > 0 {
|
|
||||||
result["recent_summaries"] = summaries
|
|
||||||
} else {
|
|
||||||
warn("recent_summaries", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if summaries, err := t.store.LoadRecentSummaries(a.Chapter, profile.SummaryWindow); err == nil && len(summaries) > 0 {
|
if summaries, err := t.store.LoadRecentSummaries(a.Chapter, profile.SummaryWindow); err == nil && len(summaries) > 0 {
|
||||||
result["recent_summaries"] = summaries
|
result["recent_summaries"] = summaries
|
||||||
@@ -143,33 +137,17 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间线:Layered 用窗口,其他按策略
|
// 时间线:窗口加载
|
||||||
if profile.FullContext {
|
if timeline, err := t.store.LoadRecentTimeline(a.Chapter, profile.TimelineWindow); err == nil && len(timeline) > 0 {
|
||||||
if timeline, err := t.store.LoadTimeline(); err == nil && len(timeline) > 0 {
|
result["timeline"] = timeline
|
||||||
result["timeline"] = timeline
|
|
||||||
} else {
|
|
||||||
warn("timeline", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if timeline, err := t.store.LoadRecentTimeline(a.Chapter, profile.TimelineWindow); err == nil && len(timeline) > 0 {
|
warn("timeline", err)
|
||||||
result["timeline"] = timeline
|
|
||||||
} else {
|
|
||||||
warn("timeline", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// foreshadow:短篇全量,否则只取未回收条目
|
// foreshadow:只取未回收条目
|
||||||
if profile.FullContext {
|
if foreshadow, err := t.store.LoadActiveForeshadow(); err == nil && len(foreshadow) > 0 {
|
||||||
if foreshadow, err := t.store.LoadForeshadowLedger(); err == nil && len(foreshadow) > 0 {
|
result["foreshadow_ledger"] = foreshadow
|
||||||
result["foreshadow_ledger"] = foreshadow
|
|
||||||
} else {
|
|
||||||
warn("foreshadow_ledger", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if foreshadow, err := t.store.LoadActiveForeshadow(); err == nil && len(foreshadow) > 0 {
|
warn("foreshadow_ledger", err)
|
||||||
result["foreshadow_ledger"] = foreshadow
|
|
||||||
} else {
|
|
||||||
warn("foreshadow_ledger", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// relationships:保持全量(pair-key 去重,数据量天然可控)
|
// relationships:保持全量(pair-key 去重,数据量天然可控)
|
||||||
if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
|
if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
|
||||||
@@ -177,8 +155,8 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
|||||||
} else {
|
} else {
|
||||||
warn("relationship_state", err)
|
warn("relationship_state", err)
|
||||||
}
|
}
|
||||||
// 状态变化:最近 5 章的角色/实体状态变化
|
// 状态变化:最近 2 章
|
||||||
if changes, err := t.store.LoadRecentStateChanges(a.Chapter, 5); err == nil && len(changes) > 0 {
|
if changes, err := t.store.LoadRecentStateChanges(a.Chapter, 2); err == nil && len(changes) > 0 {
|
||||||
result["recent_state_changes"] = changes
|
result["recent_state_changes"] = changes
|
||||||
} else {
|
} else {
|
||||||
warn("recent_state_changes", err)
|
warn("recent_state_changes", err)
|
||||||
@@ -231,6 +209,17 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
|||||||
warn("chapter_plan", err)
|
warn("chapter_plan", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 前章尾部:嵌入前一章末尾 ~800 字,Writer 无需额外调用 read_chapter 获取衔接上文
|
||||||
|
if a.Chapter > 1 {
|
||||||
|
if prevText, err := t.store.LoadChapterText(a.Chapter - 1); err == nil && prevText != "" {
|
||||||
|
runes := []rune(prevText)
|
||||||
|
if len(runes) > 800 {
|
||||||
|
runes = runes[len(runes)-800:]
|
||||||
|
}
|
||||||
|
result["previous_tail"] = string(runes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 风格锚点:从前文提取代表性段落
|
// 风格锚点:从前文提取代表性段落
|
||||||
if anchors := t.store.ExtractStyleAnchors(3); len(anchors) > 0 {
|
if anchors := t.store.ExtractStyleAnchors(3); len(anchors) > 0 {
|
||||||
result["style_anchors"] = anchors
|
result["style_anchors"] = anchors
|
||||||
@@ -288,11 +277,20 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
|||||||
warn("volume_summaries", err)
|
warn("volume_summaries", err)
|
||||||
}
|
}
|
||||||
result["references"] = t.architectReferences()
|
result["references"] = t.architectReferences()
|
||||||
|
|
||||||
|
// 基础设定完备性检查
|
||||||
|
result["foundation_status"] = t.foundationStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(warnings) > 0 {
|
if len(warnings) > 0 {
|
||||||
result["_warnings"] = warnings
|
result["_warnings"] = warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优先级预算:总大小超过阈值时自动裁剪低优先级数据
|
||||||
|
if a.Chapter > 0 {
|
||||||
|
trimByBudget(result, 100*1024) // 100KB 预算
|
||||||
|
}
|
||||||
|
|
||||||
result["_loading_summary"] = buildLoadingSummary(result, a.Chapter)
|
result["_loading_summary"] = buildLoadingSummary(result, a.Chapter)
|
||||||
return json.Marshal(result)
|
return json.Marshal(result)
|
||||||
}
|
}
|
||||||
@@ -363,6 +361,9 @@ func buildLoadingSummary(result map[string]any, chapter int) string {
|
|||||||
if n := countSlice("recent_state_changes"); n > 0 {
|
if n := countSlice("recent_state_changes"); n > 0 {
|
||||||
items = append(items, fmt.Sprintf("状态变化:%d", n))
|
items = append(items, fmt.Sprintf("状态变化:%d", n))
|
||||||
}
|
}
|
||||||
|
if _, ok := result["previous_tail"]; ok {
|
||||||
|
items = append(items, "前章尾部:ok")
|
||||||
|
}
|
||||||
|
|
||||||
// 参考资料
|
// 参考资料
|
||||||
if refs, ok := result["references"].(map[string]string); ok && len(refs) > 0 {
|
if refs, ok := result["references"].(map[string]string); ok && len(refs) > 0 {
|
||||||
@@ -371,6 +372,9 @@ func buildLoadingSummary(result map[string]any, chapter int) string {
|
|||||||
if warnings, ok := result["_warnings"].([]string); ok && len(warnings) > 0 {
|
if warnings, ok := result["_warnings"].([]string); ok && len(warnings) > 0 {
|
||||||
items = append(items, fmt.Sprintf("告警:%d", len(warnings)))
|
items = append(items, fmt.Sprintf("告警:%d", len(warnings)))
|
||||||
}
|
}
|
||||||
|
if trimmed, ok := result["_trimmed"].([]string); ok && len(trimmed) > 0 {
|
||||||
|
items = append(items, fmt.Sprintf("裁剪:%s", strings.Join(trimmed, ",")))
|
||||||
|
}
|
||||||
|
|
||||||
if len(items) > 0 {
|
if len(items) > 0 {
|
||||||
parts = append(parts, strings.Join(items, " "))
|
parts = append(parts, strings.Join(items, " "))
|
||||||
@@ -520,15 +524,17 @@ func (t *ContextTool) writerReferences(chapter int) map[string]string {
|
|||||||
refs[k] = v
|
refs[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 始终加载的核心参考
|
// 渐进式加载:始终保留核心参考,前 3 章额外加载完整写作指南
|
||||||
add("chapter_guide", t.refs.ChapterGuide)
|
add("consistency", t.refs.Consistency)
|
||||||
add("hook_techniques", t.refs.HookTechniques)
|
add("hook_techniques", t.refs.HookTechniques)
|
||||||
add("quality_checklist", t.refs.QualityChecklist)
|
add("quality_checklist", t.refs.QualityChecklist)
|
||||||
add("consistency", t.refs.Consistency)
|
if chapter <= 3 {
|
||||||
add("dialogue_writing", t.refs.DialogueWriting)
|
add("chapter_guide", t.refs.ChapterGuide)
|
||||||
add("style_reference", t.refs.StyleReference)
|
add("dialogue_writing", t.refs.DialogueWriting)
|
||||||
|
add("style_reference", t.refs.StyleReference)
|
||||||
|
}
|
||||||
|
|
||||||
// 仅首章加载的补充参考(后续章节不再需要)
|
// 仅首章加载的补充参考
|
||||||
if chapter <= 1 {
|
if chapter <= 1 {
|
||||||
add("chapter_template", t.refs.ChapterTemplate)
|
add("chapter_template", t.refs.ChapterTemplate)
|
||||||
add("content_expansion", t.refs.ContentExpansion)
|
add("content_expansion", t.refs.ContentExpansion)
|
||||||
@@ -551,6 +557,29 @@ func (t *ContextTool) architectReferences() map[string]string {
|
|||||||
return refs
|
return refs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// foundationStatus 检查基础设定的完备性,返回缺失项列表。
|
||||||
|
func (t *ContextTool) foundationStatus() map[string]any {
|
||||||
|
status := map[string]any{"ready": true}
|
||||||
|
var missing []string
|
||||||
|
if p, _ := t.store.LoadPremise(); p == "" {
|
||||||
|
missing = append(missing, "premise")
|
||||||
|
}
|
||||||
|
if o, _ := t.store.LoadOutline(); len(o) == 0 {
|
||||||
|
missing = append(missing, "outline")
|
||||||
|
}
|
||||||
|
if c, _ := t.store.LoadCharacters(); len(c) == 0 {
|
||||||
|
missing = append(missing, "characters")
|
||||||
|
}
|
||||||
|
if r, _ := t.store.LoadWorldRules(); len(r) == 0 {
|
||||||
|
missing = append(missing, "world_rules")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
status["ready"] = false
|
||||||
|
status["missing"] = missing
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
// ContextSummary 返回当前状态的简要摘要(供日志使用)。
|
// ContextSummary 返回当前状态的简要摘要(供日志使用)。
|
||||||
func (t *ContextTool) ContextSummary() string {
|
func (t *ContextTool) ContextSummary() string {
|
||||||
var parts []string
|
var parts []string
|
||||||
@@ -568,3 +597,43 @@ func (t *ContextTool) ContextSummary() string {
|
|||||||
}
|
}
|
||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trimByBudget 按优先级裁剪 result,使 JSON 总大小不超过 budget 字节。
|
||||||
|
// 优先级(从低到高):references < voice_samples < style_anchors < previous_tail < timeline
|
||||||
|
// < recent_state_changes < foreshadow_ledger < relationship_state < 其余(不裁剪)
|
||||||
|
// 裁剪的 key 会记录到 result["_trimmed"] 供日志排查。
|
||||||
|
func trimByBudget(result map[string]any, budget int) {
|
||||||
|
// 先测量当前大小
|
||||||
|
data, err := json.Marshal(result)
|
||||||
|
if err != nil || len(data) <= budget {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按优先级从低到高列出可裁剪的 key
|
||||||
|
trimOrder := []string{
|
||||||
|
"references",
|
||||||
|
"voice_samples",
|
||||||
|
"style_anchors",
|
||||||
|
"previous_tail",
|
||||||
|
"timeline",
|
||||||
|
"recent_state_changes",
|
||||||
|
"foreshadow_ledger",
|
||||||
|
"relationship_state",
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed []string
|
||||||
|
for _, key := range trimOrder {
|
||||||
|
if _, ok := result[key]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(result, key)
|
||||||
|
trimmed = append(trimmed, key)
|
||||||
|
data, err = json.Marshal(result)
|
||||||
|
if err != nil || len(data) <= budget {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(trimmed) > 0 {
|
||||||
|
result["_trimmed"] = trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,13 +59,14 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := map[string]any{"saved": true, "type": a.Type, "scale": a.Scale}
|
||||||
|
|
||||||
switch a.Type {
|
switch a.Type {
|
||||||
case "premise":
|
case "premise":
|
||||||
if err := t.store.SavePremise(content); err != nil {
|
if err := t.store.SavePremise(content); err != nil {
|
||||||
return nil, fmt.Errorf("save premise: %w", err)
|
return nil, fmt.Errorf("save premise: %w", err)
|
||||||
}
|
}
|
||||||
_ = t.store.UpdatePhase(domain.PhasePremise)
|
_ = t.store.UpdatePhase(domain.PhasePremise)
|
||||||
return json.Marshal(map[string]any{"saved": true, "type": "premise", "scale": a.Scale})
|
|
||||||
|
|
||||||
case "outline":
|
case "outline":
|
||||||
var entries []domain.OutlineEntry
|
var entries []domain.OutlineEntry
|
||||||
@@ -76,14 +77,13 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
|||||||
return nil, fmt.Errorf("save outline: %w", err)
|
return nil, fmt.Errorf("save outline: %w", err)
|
||||||
}
|
}
|
||||||
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
||||||
// 根据大纲长度自动设定总章节数
|
|
||||||
_ = t.store.SetTotalChapters(len(entries))
|
_ = t.store.SetTotalChapters(len(entries))
|
||||||
if domain.PlanningTier(a.Scale) != domain.PlanningTierLong {
|
if domain.PlanningTier(a.Scale) != domain.PlanningTierLong {
|
||||||
_ = t.store.SetLayered(false)
|
_ = t.store.SetLayered(false)
|
||||||
_ = t.store.UpdateVolumeArc(0, 0)
|
_ = t.store.UpdateVolumeArc(0, 0)
|
||||||
_ = t.store.ClearLayeredOutline()
|
_ = t.store.ClearLayeredOutline()
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{"saved": true, "type": "outline", "chapters": len(entries), "scale": a.Scale})
|
result["chapters"] = len(entries)
|
||||||
|
|
||||||
case "layered_outline":
|
case "layered_outline":
|
||||||
var volumes []domain.VolumeOutline
|
var volumes []domain.VolumeOutline
|
||||||
@@ -93,7 +93,6 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
|||||||
if err := t.store.SaveLayeredOutline(volumes); err != nil {
|
if err := t.store.SaveLayeredOutline(volumes); err != nil {
|
||||||
return nil, fmt.Errorf("save layered_outline: %w", err)
|
return nil, fmt.Errorf("save layered_outline: %w", err)
|
||||||
}
|
}
|
||||||
// 展开为扁平大纲,兼容现有 GetChapterOutline
|
|
||||||
flat := domain.FlattenOutline(volumes)
|
flat := domain.FlattenOutline(volumes)
|
||||||
if err := t.store.SaveOutline(flat); err != nil {
|
if err := t.store.SaveOutline(flat); err != nil {
|
||||||
return nil, fmt.Errorf("save flattened outline: %w", err)
|
return nil, fmt.Errorf("save flattened outline: %w", err)
|
||||||
@@ -105,11 +104,8 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
|||||||
if len(volumes) > 0 && len(volumes[0].Arcs) > 0 {
|
if len(volumes) > 0 && len(volumes[0].Arcs) > 0 {
|
||||||
_ = t.store.UpdateVolumeArc(volumes[0].Index, volumes[0].Arcs[0].Index)
|
_ = t.store.UpdateVolumeArc(volumes[0].Index, volumes[0].Arcs[0].Index)
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{
|
result["volumes"] = len(volumes)
|
||||||
"saved": true, "type": "layered_outline",
|
result["chapters"] = total
|
||||||
"volumes": len(volumes), "chapters": total,
|
|
||||||
"scale": a.Scale,
|
|
||||||
})
|
|
||||||
|
|
||||||
case "characters":
|
case "characters":
|
||||||
var chars []domain.Character
|
var chars []domain.Character
|
||||||
@@ -119,7 +115,7 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
|||||||
if err := t.store.SaveCharacters(chars); err != nil {
|
if err := t.store.SaveCharacters(chars); err != nil {
|
||||||
return nil, fmt.Errorf("save characters: %w", err)
|
return nil, fmt.Errorf("save characters: %w", err)
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{"saved": true, "type": "characters", "count": len(chars), "scale": a.Scale})
|
result["count"] = len(chars)
|
||||||
|
|
||||||
case "world_rules":
|
case "world_rules":
|
||||||
var rules []domain.WorldRule
|
var rules []domain.WorldRule
|
||||||
@@ -129,11 +125,15 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
|||||||
if err := t.store.SaveWorldRules(rules); err != nil {
|
if err := t.store.SaveWorldRules(rules); err != nil {
|
||||||
return nil, fmt.Errorf("save world_rules: %w", err)
|
return nil, fmt.Errorf("save world_rules: %w", err)
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{"saved": true, "type": "world_rules", "count": len(rules), "scale": a.Scale})
|
result["count"] = len(rules)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown type %q, expected premise/outline/layered_outline/characters/world_rules", a.Type)
|
return nil, fmt.Errorf("unknown type %q, expected premise/outline/layered_outline/characters/world_rules", a.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 返回剩余未完成项,引导 Architect 继续
|
||||||
|
result["remaining"] = t.remaining()
|
||||||
|
return json.Marshal(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeFoundationContent(raw json.RawMessage) (string, error) {
|
func normalizeFoundationContent(raw json.RawMessage) (string, error) {
|
||||||
@@ -151,3 +151,21 @@ func normalizeFoundationContent(raw json.RawMessage) (string, error) {
|
|||||||
}
|
}
|
||||||
return string(raw), nil
|
return string(raw), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remaining 检查基础设定中还缺少哪些必要项。
|
||||||
|
func (t *SaveFoundationTool) remaining() []string {
|
||||||
|
var missing []string
|
||||||
|
if p, _ := t.store.LoadPremise(); p == "" {
|
||||||
|
missing = append(missing, "premise")
|
||||||
|
}
|
||||||
|
if o, _ := t.store.LoadOutline(); len(o) == 0 {
|
||||||
|
missing = append(missing, "outline")
|
||||||
|
}
|
||||||
|
if c, _ := t.store.LoadCharacters(); len(c) == 0 {
|
||||||
|
missing = append(missing, "characters")
|
||||||
|
}
|
||||||
|
if r, _ := t.store.LoadWorldRules(); len(r) == 0 {
|
||||||
|
missing = append(missing, "world_rules")
|
||||||
|
}
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user