feat: provider manager, cancellation, session summaries (store+agent)

This commit is contained in:
OpenClaw Dev 2026-10-25 04:46:40 +01:00
parent 406a523d54
commit f1b2093b22
10 changed files with 883 additions and 37 deletions

View file

@ -2,6 +2,8 @@ package agent
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"testing"
@ -91,3 +93,116 @@ func TestHistoryDropsOrphanToolMessages(t *testing.T) {
t.Fatalf("%+v", h)
}
}
// blocking streams one delta then waits for cancellation, like a slow provider.
type blocking struct{ started chan struct{} }
func (b *blocking) Chat(ctx context.Context, _ llm.Request, d llm.DeltaFunc) (llm.Response, error) {
d("Teil")
d("antwort")
close(b.started)
<-ctx.Done()
return llm.Response{}, ctx.Err()
}
func TestCancelStoresPartialAnswer(t *testing.T) {
st := newStore(t)
ss, _ := st.CreateSession(context.Background(), "")
b := &blocking{started: make(chan struct{})}
a := &Agent{LLM: b, Store: st, Tools: tools.NewRegistry(), MaxIterations: 3}
if a.Cancel(ss.ID) {
t.Fatal("nothing should be running")
}
go func() {
<-b.started
if !a.Cancel(ss.ID) {
t.Error("cancel should find the run")
}
}()
text, err := a.Run(context.Background(), ss.ID, "erzähl was", nil)
if !errors.Is(err, ErrCancelled) || text != "Teilantwort" {
t.Fatalf("%q %v", text, err)
}
msgs, _ := st.Messages(context.Background(), ss.ID, 0)
if len(msgs) != 2 || !msgs[1].Cancelled || msgs[1].Content != "Teilantwort" {
t.Fatalf("%+v", msgs)
}
if a.Running(ss.ID) {
t.Fatal("run must be unregistered")
}
}
// recorder answers summary requests with "SUMMARY" and normal turns with "ok".
type recorder struct{ reqs []llm.Request }
func (r *recorder) Chat(_ context.Context, req llm.Request, _ llm.DeltaFunc) (llm.Response, error) {
r.reqs = append(r.reqs, req)
if strings.Contains(req.Messages[0].Content, "compress chat histories") {
return llm.Response{Content: "SUMMARY"}, nil
}
return llm.Response{Content: "ok"}, nil
}
func TestAutoSummarize(t *testing.T) {
st := newStore(t)
ctx := context.Background()
ss, _ := st.CreateSession(ctx, "")
p := &recorder{}
a := &Agent{LLM: p, Store: st, Tools: tools.NewRegistry(), MaxIterations: 2, SystemPrompt: "sys",
Summarize: SummarizeConfig{Enabled: true, MaxMessages: 6, KeepRecent: 2}}
for i := 0; i < 3; i++ { // 6 messages -> at threshold, no summary yet
a.Run(ctx, ss.ID, fmt.Sprintf("frage %d", i), nil)
}
if _, err := st.LatestSummary(ctx, ss.ID); err == nil {
t.Fatal("summary created too early")
}
var events []string
a.Run(ctx, ss.ID, "frage 3", func(e Event) { events = append(events, e.Type) })
sm, err := st.LatestSummary(ctx, ss.ID)
if err != nil || sm.Content != "SUMMARY" {
t.Fatalf("%+v %v", sm, err)
}
if events[0] != "summarized" {
t.Fatalf("%v", events)
}
last := p.reqs[len(p.reqs)-1].Messages
if last[1].Role != llm.RoleSystem || !strings.Contains(last[1].Content, "SUMMARY") {
t.Fatalf("summary not in context: %+v", last)
}
// the verbatim tail starts at a user message and older messages are not sent
if last[2].Role != llm.RoleUser || strings.Contains(fmt.Sprint(last), "frage 0") {
t.Fatalf("bad window: %+v", last)
}
if all, _ := st.Messages(ctx, ss.ID, 0); len(all) != 8 {
t.Fatalf("originals must be kept, got %d", len(all))
}
}
func TestAutoSummarizeByTokens(t *testing.T) {
st := newStore(t)
ctx := context.Background()
ss, _ := st.CreateSession(ctx, "")
a := &Agent{LLM: &recorder{}, Store: st, Tools: tools.NewRegistry(), MaxIterations: 2,
Summarize: SummarizeConfig{Enabled: true, MaxMessages: 1000, MaxTokens: 50, KeepRecent: 1}}
a.Run(ctx, ss.ID, strings.Repeat("lang ", 100), nil)
a.Run(ctx, ss.ID, "kurz", nil)
if _, err := st.LatestSummary(ctx, ss.ID); err != nil {
t.Fatal("token threshold should trigger summary")
}
}
func TestSummarizeNowManual(t *testing.T) {
st := newStore(t)
ctx := context.Background()
ss, _ := st.CreateSession(ctx, "")
a := &Agent{LLM: &recorder{}, Store: st, Tools: tools.NewRegistry(), MaxIterations: 2, Summarize: SummarizeConfig{KeepRecent: 2}}
if sm, err := a.SummarizeNow(ctx, ss.ID); err != nil || sm != nil {
t.Fatalf("empty session: %v %v", sm, err)
}
a.Run(ctx, ss.ID, "eins", nil)
a.Run(ctx, ss.ID, "zwei", nil)
sm, err := a.SummarizeNow(ctx, ss.ID)
if err != nil || sm == nil || sm.Content != "SUMMARY" {
t.Fatalf("%v %v", sm, err)
}
}