feat: provider manager, cancellation, session summaries (store+agent)
This commit is contained in:
parent
406a523d54
commit
f1b2093b22
10 changed files with 883 additions and 37 deletions
105
internal/agent/summarize.go
Normal file
105
internal/agent/summarize.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/crypto/byteclaw/internal/llm"
|
||||
"github.com/crypto/byteclaw/internal/store"
|
||||
)
|
||||
|
||||
const summarizePrompt = `You compress chat histories. Write a concise summary of the conversation below so an assistant can continue it.
|
||||
Keep facts, user preferences, decisions, results of tool calls and open tasks. Use the language of the conversation. No preamble.`
|
||||
|
||||
// estimateTokens is a cheap approximation (≈4 characters per token).
|
||||
func estimateTokens(msgs []store.StoredMessage) int {
|
||||
n := 0
|
||||
for _, m := range msgs {
|
||||
n += len(m.Content) / 4
|
||||
for _, tc := range m.ToolCalls {
|
||||
n += len(tc.Arguments) / 4
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// SummarizeNow summarises the session regardless of thresholds (manual trigger).
|
||||
// Returns nil summary if there is nothing old enough to summarise.
|
||||
func (a *Agent) SummarizeNow(ctx context.Context, sessionID string) (*store.Summary, error) {
|
||||
l := a.sessionLock(sessionID)
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
prov, err := a.provider(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.summarizeLocked(ctx, prov, sessionID, true)
|
||||
}
|
||||
|
||||
func (a *Agent) summarizeLocked(ctx context.Context, prov llm.Provider, sessionID string, force bool) (*store.Summary, error) {
|
||||
var after int64
|
||||
prev := ""
|
||||
if sm, err := a.Store.LatestSummary(ctx, sessionID); err == nil {
|
||||
after, prev = sm.UptoMessageID, sm.Content
|
||||
} else if !errors.Is(err, store.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
msgs, err := a.Store.MessagesAfter(ctx, sessionID, after, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := a.Summarize
|
||||
if !force && len(msgs) <= cfg.MaxMessages && (cfg.MaxTokens <= 0 || estimateTokens(msgs) <= cfg.MaxTokens) {
|
||||
return nil, nil
|
||||
}
|
||||
keep := cfg.KeepRecent
|
||||
if keep < 1 {
|
||||
keep = 1
|
||||
}
|
||||
cut := len(msgs) - keep
|
||||
// The verbatim tail must start at a user message so tool call/result pairs stay together.
|
||||
for cut > 0 && cut < len(msgs) && msgs[cut].Role != llm.RoleUser {
|
||||
cut--
|
||||
}
|
||||
if cut <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var b strings.Builder
|
||||
if prev != "" {
|
||||
b.WriteString("Previous summary:\n" + prev + "\n\n")
|
||||
}
|
||||
b.WriteString("Conversation:\n")
|
||||
for _, m := range msgs[:cut] {
|
||||
switch {
|
||||
case m.Role == llm.RoleTool:
|
||||
fmt.Fprintf(&b, "tool %s result: %s\n", m.Name, clip(m.Content, 500))
|
||||
case len(m.ToolCalls) > 0:
|
||||
for _, tc := range m.ToolCalls {
|
||||
fmt.Fprintf(&b, "assistant called %s(%s)\n", tc.Name, clip(tc.Arguments, 200))
|
||||
}
|
||||
if m.Content != "" {
|
||||
fmt.Fprintf(&b, "assistant: %s\n", m.Content)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(&b, "%s: %s\n", m.Role, m.Content)
|
||||
}
|
||||
}
|
||||
resp, err := prov.Chat(ctx, llm.Request{Messages: []llm.Message{
|
||||
{Role: llm.RoleSystem, Content: summarizePrompt},
|
||||
{Role: llm.RoleUser, Content: b.String()},
|
||||
}}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("summarize: %w", err)
|
||||
}
|
||||
text := strings.TrimSpace(resp.Content)
|
||||
if text == "" {
|
||||
return nil, errors.New("summarize: empty summary")
|
||||
}
|
||||
sm, err := a.Store.AddSummary(context.WithoutCancel(ctx), sessionID, msgs[cut-1].ID, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sm, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue