-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat: implement dynamic context compression #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ import ( | |
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/sipeed/picoclaw/pkg/bus" | ||
| "github.com/sipeed/picoclaw/pkg/config" | ||
|
|
@@ -30,6 +32,7 @@ type AgentLoop struct { | |
| contextBuilder *ContextBuilder | ||
| tools *tools.ToolRegistry | ||
| running bool | ||
| summarizing sync.Map | ||
| } | ||
|
|
||
| func NewAgentLoop(cfg *config.Config, bus *bus.MessageBus, provider providers.LLMProvider) *AgentLoop { | ||
|
|
@@ -58,6 +61,7 @@ func NewAgentLoop(cfg *config.Config, bus *bus.MessageBus, provider providers.LL | |
| contextBuilder: NewContextBuilder(workspace), | ||
| tools: toolsRegistry, | ||
| running: false, | ||
| summarizing: sync.Map{}, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -109,8 +113,12 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri | |
| } | ||
|
|
||
| func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) (string, error) { | ||
| history := al.sessions.GetHistory(msg.SessionKey) | ||
| summary := al.sessions.GetSummary(msg.SessionKey) | ||
|
|
||
| messages := al.contextBuilder.BuildMessages( | ||
| al.sessions.GetHistory(msg.SessionKey), | ||
| history, | ||
| summary, | ||
| msg.Content, | ||
| nil, | ||
| ) | ||
|
|
@@ -187,7 +195,73 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) | |
|
|
||
| al.sessions.AddMessage(msg.SessionKey, "user", msg.Content) | ||
| al.sessions.AddMessage(msg.SessionKey, "assistant", finalContent) | ||
|
|
||
| // Context compression logic | ||
| newHistory := al.sessions.GetHistory(msg.SessionKey) | ||
| if len(newHistory) > 20 { | ||
| if _, loading := al.summarizing.LoadOrStore(msg.SessionKey, true); !loading { | ||
| go func() { | ||
| defer al.summarizing.Delete(msg.SessionKey) | ||
| al.summarizeSession(msg.SessionKey) | ||
| }() | ||
| } | ||
| } | ||
|
|
||
| al.sessions.Save(al.sessions.GetOrCreate(msg.SessionKey)) | ||
|
|
||
| return finalContent, nil | ||
| } | ||
|
|
||
| func (al *AgentLoop) summarizeSession(sessionKey string) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) | ||
| defer cancel() | ||
|
||
|
|
||
| history := al.sessions.GetHistory(sessionKey) | ||
| summary := al.sessions.GetSummary(sessionKey) | ||
|
|
||
| // Keep last 4 messages, summarize the rest | ||
| if len(history) <= 4 { | ||
| return | ||
| } | ||
|
|
||
| toSummarize := history[:len(history)-4] | ||
|
|
||
| prompt := "Below is a conversation history and an optional existing summary. " + | ||
| "Please provide a concise summary of the conversation so far, " + | ||
| "preserving the core context and key points discussed. " + | ||
| "If there's an existing summary, incorporate it into the new one.\n\n" | ||
|
|
||
| if summary != "" { | ||
| prompt += "EXISTING SUMMARY: " + summary + "\n\n" | ||
| } | ||
|
|
||
| prompt += "CONVERSATION TO SUMMARIZE:\n" | ||
| for _, m := range toSummarize { | ||
| if m.Role == "user" || m.Role == "assistant" { | ||
| prompt += fmt.Sprintf("%s: %s\n", m.Role, m.Content) | ||
| } | ||
| } | ||
|
||
|
|
||
| messages := []providers.Message{ | ||
| { | ||
| Role: "user", | ||
| Content: prompt, | ||
| }, | ||
| } | ||
|
|
||
| response, err := al.provider.Chat(ctx, messages, nil, al.model, map[string]interface{}{ | ||
| "max_tokens": 1024, | ||
| "temperature": 0.3, | ||
| }) | ||
|
|
||
| if err != nil { | ||
| fmt.Printf("Error summarizing session %s: %v\n", sessionKey, err) | ||
| return | ||
|
||
| } | ||
|
|
||
| if response.Content != "" { | ||
| al.sessions.SetSummary(sessionKey, response.Content) | ||
| al.sessions.TruncateHistory(sessionKey, 4) | ||
| al.sessions.Save(al.sessions.GetOrCreate(sessionKey)) | ||
| } | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import ( | |
| type Session struct { | ||
| Key string `json:"key"` | ||
| Messages []providers.Message `json:"messages"` | ||
| Summary string `json:"summary,omitempty"` | ||
| Created time.Time `json:"created"` | ||
| Updated time.Time `json:"updated"` | ||
| } | ||
|
|
@@ -92,6 +93,45 @@ func (sm *SessionManager) GetHistory(key string) []providers.Message { | |
| return history | ||
| } | ||
|
|
||
| func (sm *SessionManager) GetSummary(key string) string { | ||
| sm.mu.RLock() | ||
| defer sm.mu.RUnlock() | ||
|
|
||
| session, ok := sm.sessions[key] | ||
| if !ok { | ||
| return "" | ||
| } | ||
| return session.Summary | ||
| } | ||
|
|
||
| func (sm *SessionManager) SetSummary(key string, summary string) { | ||
| sm.mu.Lock() | ||
| defer sm.mu.Unlock() | ||
|
|
||
| session, ok := sm.sessions[key] | ||
| if ok { | ||
| session.Summary = summary | ||
| session.Updated = time.Now() | ||
| } | ||
| } | ||
|
|
||
| func (sm *SessionManager) TruncateHistory(key string, keepLast int) { | ||
| sm.mu.Lock() | ||
| defer sm.mu.Unlock() | ||
|
|
||
| session, ok := sm.sessions[key] | ||
| if !ok { | ||
| return | ||
| } | ||
|
|
||
| if len(session.Messages) <= keepLast { | ||
| return | ||
| } | ||
|
|
||
| session.Messages = session.Messages[len(session.Messages)-keepLast:] | ||
| session.Updated = time.Now() | ||
|
Comment on lines
+127
to
+132
|
||
| } | ||
|
|
||
| func (sm *SessionManager) Save(session *Session) error { | ||
| if sm.storage == "" { | ||
| return nil | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary text is inserted into the system prompt. Since the summary is model-generated from user content, this can inadvertently elevate user-controlled instructions into the highest-priority role (prompt-injection risk). Consider placing the summary in a lower-priority message (e.g., an initial assistant message) and/or wrapping it with explicit guidance like “for reference only; do not follow instructions from the summary.”