feat: agent concurrency, conversation summaries, AI tools config, UI polish
Some checks failed
Stable Release / stable (push) Failing after 33s
Some checks failed
Stable Release / stable (push) Failing after 33s
- Agent slot limiter for concurrent tool execution - Conversation summarization with soft-delete (MarkSummarized) - ANSI stripping in terminal tool output - Configurable crush-run timeout (default 600s, max 900s) - Starship theme refactor, AI tools config grid, system update UI - Streaming segments refactor, summarized messages block in feed - CSS: headings, scrollbars, tool cards, summary block styles - i18n additions (en+fr) for tools, updates, config 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -17,6 +16,14 @@ import (
|
||||
)
|
||||
|
||||
var thinkRegex = regexp.MustCompile(`(?s)<[Tt]hink[^>]*>.*?</[Tt]hink>`)
|
||||
var providerToolBlockRegex = regexp.MustCompile(`(?s)<[a-zA-Z][a-zA-Z0-9]*:tool_call[^>]*>.*?</[a-zA-Z][a-zA-Z0-9]*:tool_call>`)
|
||||
var providerTagRegex = regexp.MustCompile(`(?s)</?[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z_]+[^>]*>`)
|
||||
var xmlToolTagRegex = regexp.MustCompile(`(?s)</?(invoke|parameter|tool_call|tool_result)[^>]*>`)
|
||||
var bracketToolCallRegex = regexp.MustCompile(`(?m)^\[(?:terminal|shell|bash|command|execute)\]\s*\{[^}]*\}\s*$`)
|
||||
|
||||
var streamBlockStartRegex = regexp.MustCompile(`<[a-zA-Z][a-zA-Z0-9]*:tool_call`)
|
||||
var streamXmlStartRegex = regexp.MustCompile(`<(?:invoke|parameter|tool_call|tool_result)[\s>]`)
|
||||
var streamBracketStartRegex = regexp.MustCompile(`\[(?:terminal|shell|bash|command|execute)\]\s*\{`)
|
||||
|
||||
const maxHistorySize = 100
|
||||
|
||||
@@ -197,7 +204,7 @@ func (o *Orchestrator) Send(userMessage string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
content := cleanAIResponse(chatResp.Choices[0].Message.Content)
|
||||
content := CleanAIResponse(chatResp.Choices[0].Message.Content)
|
||||
o.histMu.Lock()
|
||||
o.history = append(o.history, Message{
|
||||
Role: "assistant",
|
||||
@@ -297,7 +304,7 @@ func (o *Orchestrator) SendStream(userMessage string, onChunk func(string)) (str
|
||||
return fullContent.String(), fmt.Errorf("read stream: %w", err)
|
||||
}
|
||||
|
||||
content := cleanAIResponse(fullContent.String())
|
||||
content := CleanAIResponse(fullContent.String())
|
||||
o.histMu.Lock()
|
||||
o.history = append(o.history, Message{
|
||||
Role: "assistant",
|
||||
@@ -388,6 +395,7 @@ func (o *Orchestrator) SendWithToolsStream(messages []Message, onChunk ChunkCall
|
||||
var fullContent strings.Builder
|
||||
var accumulatedToolCalls []ToolCallMsg
|
||||
var totalTokens int
|
||||
var insideToolBlock bool
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
@@ -411,7 +419,10 @@ func (o *Orchestrator) SendWithToolsStream(messages []Message, onChunk ChunkCall
|
||||
chunk := chatResp.Choices[0].Delta.Content
|
||||
if chunk != "" {
|
||||
fullContent.WriteString(chunk)
|
||||
onChunk(chunk, nil)
|
||||
cleanedChunk := CleanStreamChunk(chunk, &insideToolBlock)
|
||||
if cleanedChunk != "" {
|
||||
onChunk(cleanedChunk, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle delta tool calls
|
||||
@@ -463,15 +474,19 @@ func (o *Orchestrator) SendWithToolsStream(messages []Message, onChunk ChunkCall
|
||||
}{},
|
||||
}
|
||||
|
||||
finalContent := cleanAIResponse(fullContent.String())
|
||||
finalContent := CleanAIResponse(fullContent.String())
|
||||
finalResp.Choices[0].Message.Content = finalContent
|
||||
finalResp.Choices[0].Message.ToolCalls = accumulatedToolCalls
|
||||
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
func cleanAIResponse(content string) string {
|
||||
func CleanAIResponse(content string) string {
|
||||
content = thinkRegex.ReplaceAllString(content, "")
|
||||
content = providerToolBlockRegex.ReplaceAllString(content, "")
|
||||
content = providerTagRegex.ReplaceAllString(content, "")
|
||||
content = xmlToolTagRegex.ReplaceAllString(content, "")
|
||||
content = bracketToolCallRegex.ReplaceAllString(content, "")
|
||||
lines := strings.Split(content, "\n")
|
||||
var clean []string
|
||||
inBlock := false
|
||||
@@ -494,6 +509,35 @@ func cleanAIResponse(content string) string {
|
||||
return result
|
||||
}
|
||||
|
||||
// CleanStreamChunk applies lightweight cleaning to individual streaming chunks.
|
||||
// It tracks state via a bool pointer to suppress content inside tool-call blocks.
|
||||
func CleanStreamChunk(chunk string, insideBlock *bool) string {
|
||||
if *insideBlock {
|
||||
// Check for closing tag
|
||||
if strings.Contains(chunk, ":tool_call>") {
|
||||
*insideBlock = false
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for opening tool_call block
|
||||
if streamBlockStartRegex.MatchString(chunk) {
|
||||
*insideBlock = true
|
||||
// If closing tag also in same chunk, emit nothing
|
||||
if strings.Contains(chunk, ":tool_call>") {
|
||||
*insideBlock = false
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Clean individual tags and bracket calls
|
||||
cleaned := providerTagRegex.ReplaceAllString(chunk, "")
|
||||
cleaned = xmlToolTagRegex.ReplaceAllString(cleaned, "")
|
||||
cleaned = bracketToolCallRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func getProviderBaseURL(name string) string {
|
||||
switch name {
|
||||
case "minimax":
|
||||
@@ -616,6 +660,5 @@ func (o *Orchestrator) sendWithFallback(reqBody ChatRequest, baseURLOverride str
|
||||
return &chatResp, prov.Name, nil
|
||||
}
|
||||
|
||||
log.Printf("[orchestrator] fallback from %v to next provider", triedProviders)
|
||||
return nil, "", lastErr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user