mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 16:10:59 +00:00
cd2e407b29
sanitizeHistory now returns dropped count so callers know when orphaned tool_use/tool_result messages were removed. When orphans are found in buildMessages, the full session history is sanitized and persisted, preventing repeated warnings on every request. - Add SetHistory() to SessionStore interface and both implementations - Adapt memoryflush caller to new two-return signature - Change sanitize log level from Warn to Debug
208 lines
5.9 KiB
Go
208 lines
5.9 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
)
|
|
|
|
// Default memory flush prompts matching TS memory-flush.ts.
|
|
const (
|
|
DefaultMemoryFlushPrompt = "Pre-compaction memory flush. " +
|
|
"Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). " +
|
|
"IMPORTANT: If the file already exists, APPEND new content only and do not overwrite existing entries. " +
|
|
"If nothing to store, reply with NO_REPLY."
|
|
|
|
DefaultMemoryFlushSystemPrompt = "Pre-compaction memory flush turn. " +
|
|
"The session is near auto-compaction; capture durable memories to disk. " +
|
|
"You may reply, but usually NO_REPLY is correct."
|
|
)
|
|
|
|
// MemoryFlushSettings holds resolved flush config with defaults applied.
|
|
type MemoryFlushSettings struct {
|
|
Enabled bool
|
|
Prompt string
|
|
SystemPrompt string
|
|
}
|
|
|
|
// ResolveMemoryFlushSettings resolves flush settings from config, applying defaults.
|
|
// Returns nil if disabled.
|
|
func ResolveMemoryFlushSettings(compaction *config.CompactionConfig) *MemoryFlushSettings {
|
|
if compaction == nil || compaction.MemoryFlush == nil {
|
|
// Default: enabled
|
|
return &MemoryFlushSettings{
|
|
Enabled: true,
|
|
Prompt: DefaultMemoryFlushPrompt,
|
|
SystemPrompt: DefaultMemoryFlushSystemPrompt,
|
|
}
|
|
}
|
|
|
|
mf := compaction.MemoryFlush
|
|
if mf.Enabled != nil && !*mf.Enabled {
|
|
return nil
|
|
}
|
|
|
|
settings := &MemoryFlushSettings{
|
|
Enabled: true,
|
|
Prompt: DefaultMemoryFlushPrompt,
|
|
SystemPrompt: DefaultMemoryFlushSystemPrompt,
|
|
}
|
|
|
|
if mf.Prompt != "" {
|
|
settings.Prompt = mf.Prompt
|
|
}
|
|
if mf.SystemPrompt != "" {
|
|
settings.SystemPrompt = mf.SystemPrompt
|
|
}
|
|
|
|
return settings
|
|
}
|
|
|
|
// shouldRunMemoryFlush checks whether a memory flush should run before compaction.
|
|
// Flush always runs when compaction triggers (called inside maybeSummarize),
|
|
// gated only by enabled/memory checks and a dedup guard per compaction cycle.
|
|
func (l *Loop) shouldRunMemoryFlush(sessionKey string, totalTokens int, settings *MemoryFlushSettings) bool {
|
|
if settings == nil || !settings.Enabled || !l.hasMemory {
|
|
return false
|
|
}
|
|
|
|
if totalTokens <= 0 {
|
|
return false
|
|
}
|
|
|
|
// Deduplication: skip if already flushed in this compaction cycle.
|
|
compactionCount := l.sessions.GetCompactionCount(sessionKey)
|
|
lastFlushAt := l.sessions.GetMemoryFlushCompactionCount(sessionKey)
|
|
if lastFlushAt >= 0 && lastFlushAt == compactionCount {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// runMemoryFlush executes a memory flush turn: sends flush prompt to LLM with tools
|
|
// so it can write memory files. Matching TS agent-runner-memory.ts.
|
|
func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings *MemoryFlushSettings) {
|
|
slog.Info("memory flush: starting", "session", sessionKey)
|
|
|
|
flushCtx, cancel := context.WithTimeout(ctx, 90*time.Second)
|
|
defer cancel()
|
|
|
|
// Build messages: system prompt + history summary + flush prompt
|
|
history := l.sessions.GetHistory(sessionKey)
|
|
summary := l.sessions.GetSummary(sessionKey)
|
|
|
|
var messages []providers.Message
|
|
|
|
// System prompt: combine agent's normal system prompt context with flush system prompt
|
|
systemPrompt := BuildSystemPrompt(SystemPromptConfig{
|
|
AgentID: l.id,
|
|
Model: l.model,
|
|
Workspace: l.workspace,
|
|
Mode: PromptMinimal,
|
|
ToolNames: l.filteredToolNames(),
|
|
HasMemory: l.hasMemory,
|
|
})
|
|
systemPrompt += "\n\n" + settings.SystemPrompt
|
|
|
|
messages = append(messages, providers.Message{
|
|
Role: "system",
|
|
Content: systemPrompt,
|
|
})
|
|
|
|
// Include conversation summary for context
|
|
if summary != "" {
|
|
messages = append(messages, providers.Message{
|
|
Role: "user",
|
|
Content: fmt.Sprintf("[Previous conversation summary]\n%s", summary),
|
|
})
|
|
messages = append(messages, providers.Message{
|
|
Role: "assistant",
|
|
Content: "Understood.",
|
|
})
|
|
}
|
|
|
|
// Include recent history (last 10 messages for context)
|
|
recentHistory := history
|
|
if len(recentHistory) > 10 {
|
|
recentHistory = recentHistory[len(recentHistory)-10:]
|
|
}
|
|
sanitized, _ := sanitizeHistory(recentHistory)
|
|
messages = append(messages, sanitized...)
|
|
|
|
// Flush prompt
|
|
messages = append(messages, providers.Message{
|
|
Role: "user",
|
|
Content: settings.Prompt,
|
|
})
|
|
|
|
// Build tool list — only file tools needed for memory flush
|
|
var toolDefs []providers.ToolDefinition
|
|
if l.toolPolicy != nil {
|
|
toolDefs = l.toolPolicy.FilterTools(l.tools, l.id, l.provider.Name(), nil, nil, false, false)
|
|
} else {
|
|
toolDefs = l.tools.ProviderDefs()
|
|
}
|
|
|
|
// Run LLM iteration loop (max 5 iterations for flush)
|
|
maxFlushIter := 5
|
|
for range maxFlushIter {
|
|
resp, err := l.provider.Chat(flushCtx, providers.ChatRequest{
|
|
Messages: messages,
|
|
Tools: toolDefs,
|
|
Model: l.model,
|
|
Options: map[string]any{
|
|
"max_tokens": 4096,
|
|
"temperature": 0.3,
|
|
},
|
|
})
|
|
if err != nil {
|
|
slog.Warn("memory flush: LLM call failed", "error", err)
|
|
break
|
|
}
|
|
|
|
// No tool calls → done
|
|
if len(resp.ToolCalls) == 0 {
|
|
content := SanitizeAssistantContent(resp.Content)
|
|
if IsSilentReply(content) {
|
|
slog.Info("memory flush: NO_REPLY (nothing to save)")
|
|
} else if content != "" {
|
|
slog.Info("memory flush: completed with response", "content_len", len(content))
|
|
}
|
|
break
|
|
}
|
|
|
|
// Process tool calls
|
|
assistantMsg := providers.Message{
|
|
Role: "assistant",
|
|
Content: resp.Content,
|
|
ToolCalls: resp.ToolCalls,
|
|
}
|
|
messages = append(messages, assistantMsg)
|
|
|
|
for _, tc := range resp.ToolCalls {
|
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
|
slog.Info("memory flush: tool call", "tool", tc.Name, "args_len", len(argsJSON))
|
|
|
|
result := l.tools.ExecuteWithContext(flushCtx, tc.Name, tc.Arguments, "", "", "", sessionKey, nil)
|
|
|
|
messages = append(messages, providers.Message{
|
|
Role: "tool",
|
|
Content: result.ForLLM,
|
|
ToolCallID: tc.ID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Mark flush as done
|
|
l.sessions.SetMemoryFlushDone(sessionKey)
|
|
l.sessions.Save(sessionKey)
|
|
|
|
slog.Info("memory flush: completed", "session", sessionKey)
|
|
}
|