13b2f5e14d
Removes five unused funcs/vars/fields that golangci-lint had been flagging (anthropic.toolCallDoneEvent, mistral.translateMessages, hook.newError, subprocess.vibeParser.lastAssistantMsgID, tui.cBase), two ineffectual assignments (tui/rendering.go visible-window loop, subprocess stream_test setup), and a stale if/HasPrefix that's now a strings.TrimPrefix. Wires errcheck onto every subprocess / stream lifecycle path so a failed close or shutdown is at least logged rather than silently dropped: - engine/loop.go: stream.Close on both the error and success paths - mcp/manager.go: Shutdown when StartAll partial-fails; Transport close after Initialize failure - mcp/transport.go: stdin.Close + syscall.Kill on graceful-timeout fallback - slm/download.go: Close propagated as a named-return error on the success path; explicitly discarded on the rollback path - slm/classifier.go, slm/manager.go, hook/prompt.go, context/summarize.go, config/write.go, cmd/gnoma/main.go, tool/fs/grep.go: explicit ignores or error logging on Close / Shutdown / WalkDir / Scanln Production-code errcheck and ineffassign are now zero. Remaining golangci-lint output is test-only Close-in-defer noise plus stylistic staticcheck QF suggestions, left alone.
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
package context
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"somegit.dev/Owlibou/gnoma/internal/message"
|
|
"somegit.dev/Owlibou/gnoma/internal/provider"
|
|
"somegit.dev/Owlibou/gnoma/internal/stream"
|
|
)
|
|
|
|
const summarySystemPrompt = `You are a conversation summarizer. Condense the following conversation into a brief summary that preserves:
|
|
- Key decisions made
|
|
- Important file paths and code changes
|
|
- Tool outputs and their results
|
|
- Current task context and progress
|
|
- Any errors or blockers encountered
|
|
|
|
Be concise but preserve all critical details the assistant needs to continue the conversation coherently.
|
|
Output only the summary, no preamble.`
|
|
|
|
// SummarizeStrategy uses the LLM to create a summary of older messages.
|
|
// More expensive than truncation but preserves context better.
|
|
type SummarizeStrategy struct {
|
|
Provider provider.Provider
|
|
Model string // model to use for summarization (empty = provider default)
|
|
MaxTokens int64 // max tokens for summary output
|
|
}
|
|
|
|
func NewSummarizeStrategy(prov provider.Provider) *SummarizeStrategy {
|
|
return &SummarizeStrategy{
|
|
Provider: prov,
|
|
MaxTokens: 2048,
|
|
}
|
|
}
|
|
|
|
func (s *SummarizeStrategy) Compact(messages []message.Message, budget int64) ([]message.Message, error) {
|
|
if len(messages) <= 4 {
|
|
return messages, nil
|
|
}
|
|
|
|
// Separate system prompt from history
|
|
var systemMsgs []message.Message
|
|
var history []message.Message
|
|
|
|
for i, m := range messages {
|
|
if i == 0 && m.Role == message.RoleSystem {
|
|
systemMsgs = append(systemMsgs, m)
|
|
} else {
|
|
history = append(history, m)
|
|
}
|
|
}
|
|
|
|
if len(history) <= 4 {
|
|
return messages, nil
|
|
}
|
|
|
|
// Split: old messages to summarize, recent to keep.
|
|
// Adjust split to never orphan tool results — the assistant message with
|
|
// matching tool calls must stay in the recent window with its results.
|
|
keepRecent := 6
|
|
if keepRecent > len(history) {
|
|
keepRecent = len(history)
|
|
}
|
|
splitAt := safeSplitPoint(history, len(history)-keepRecent)
|
|
oldMessages := history[:splitAt]
|
|
recentMessages := history[splitAt:]
|
|
|
|
// Build conversation text for summarization
|
|
var convText strings.Builder
|
|
for _, m := range oldMessages {
|
|
convText.WriteString(fmt.Sprintf("[%s]: %s\n\n", m.Role, m.TextContent()))
|
|
}
|
|
|
|
// Call LLM to summarize
|
|
summary, err := s.callSummarize(convText.String())
|
|
if err != nil {
|
|
// Fall back to truncation on failure
|
|
trunc := &TruncateStrategy{KeepRecent: keepRecent}
|
|
return trunc.Compact(messages, budget)
|
|
}
|
|
|
|
// Build new history: system + summary marker + recent
|
|
summaryMsg := message.NewUserText(fmt.Sprintf("[Conversation summary — %d earlier messages condensed]\n\n%s", len(oldMessages), summary))
|
|
ackMsg := message.NewAssistantText("Understood, I have the context from the summary. Continuing from here.")
|
|
|
|
result := append(systemMsgs, summaryMsg, ackMsg)
|
|
result = append(result, recentMessages...)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *SummarizeStrategy) callSummarize(conversationText string) (string, error) {
|
|
if s.Provider == nil {
|
|
return "", fmt.Errorf("no provider for summarization")
|
|
}
|
|
|
|
req := provider.Request{
|
|
Model: s.Model,
|
|
SystemPrompt: summarySystemPrompt,
|
|
Messages: []message.Message{
|
|
message.NewUserText("Summarize this conversation:\n\n" + conversationText),
|
|
},
|
|
MaxTokens: s.MaxTokens,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
str, err := s.Provider.Stream(ctx, req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("summarization stream: %w", err)
|
|
}
|
|
defer func() { _ = str.Close() }()
|
|
|
|
// Consume stream, collect text
|
|
var result strings.Builder
|
|
for str.Next() {
|
|
evt := str.Current()
|
|
if evt.Type == stream.EventTextDelta {
|
|
result.WriteString(evt.Text)
|
|
}
|
|
}
|
|
if err := str.Err(); err != nil {
|
|
return "", fmt.Errorf("summarization stream error: %w", err)
|
|
}
|
|
|
|
summary := strings.TrimSpace(result.String())
|
|
if summary == "" {
|
|
return "", fmt.Errorf("empty summary returned")
|
|
}
|
|
|
|
return summary, nil
|
|
}
|