perf: 7 additional token optimizations to minimize API costs
1. Truncate tool results in session history (500 chars) - prevents re-sending full 10K-20K tool outputs on every subsequent LLM call 2. Limit list_dir to 200 entries - prevents huge directory listings 3. Truncate subagent ForLLM to 3000 chars - caps unbounded output 4. Detect binary files in read_file - refuse to read images/binaries that would waste tokens on garbage data 5. web_fetch: compact text format instead of JSON MarshalIndent 6. Lower summarization threshold from 20 to 12 messages 7. Limit bootstrap files to 4K chars each in system prompt Made-with: Cursor
This commit is contained in:
parent
a99cf50f27
commit
7359fb51a4
5 changed files with 74 additions and 21 deletions
|
|
@ -333,6 +333,8 @@ func skillFilesModifiedSince(skillsDir string, t time.Time) bool {
|
|||
return changed
|
||||
}
|
||||
|
||||
const maxBootstrapFileChars = 4096
|
||||
|
||||
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||
bootstrapFiles := []string{
|
||||
"AGENTS.md",
|
||||
|
|
@ -345,7 +347,11 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
|||
for _, filename := range bootstrapFiles {
|
||||
filePath := filepath.Join(cb.workspace, filename)
|
||||
if data, err := os.ReadFile(filePath); err == nil {
|
||||
fmt.Fprintf(&sb, "## %s\n\n%s\n\n", filename, data)
|
||||
content := string(data)
|
||||
if len(content) > maxBootstrapFileChars {
|
||||
content = content[:maxBootstrapFileChars] + "\n... (truncated)"
|
||||
}
|
||||
fmt.Fprintf(&sb, "## %s\n\n%s\n\n", filename, content)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -969,8 +969,19 @@ func (al *AgentLoop) runLLMIteration(
|
|||
}
|
||||
messages = append(messages, toolResultMsg)
|
||||
|
||||
// Save tool result message to session
|
||||
agent.Sessions.AddFullMessage(opts.SessionKey, toolResultMsg)
|
||||
// Save a truncated version of tool results to session history.
|
||||
// Full results are already in `messages` for the current iteration;
|
||||
// persisting truncated versions prevents context bloat on reload.
|
||||
truncatedContent := contentForLLM
|
||||
const maxToolResultHistory = 500
|
||||
if len(truncatedContent) > maxToolResultHistory {
|
||||
truncatedContent = truncatedContent[:maxToolResultHistory] + fmt.Sprintf("... (truncated, %d chars total)", len(contentForLLM))
|
||||
}
|
||||
agent.Sessions.AddFullMessage(opts.SessionKey, providers.Message{
|
||||
Role: "tool",
|
||||
Content: truncatedContent,
|
||||
ToolCallID: tc.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1014,7 +1025,7 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
|
|||
tokenEstimate := al.estimateTokens(newHistory)
|
||||
threshold := agent.ContextWindow * 75 / 100
|
||||
|
||||
if len(newHistory) > 20 || tokenEstimate > threshold {
|
||||
if len(newHistory) > 12 || tokenEstimate > threshold {
|
||||
summarizeKey := agent.ID + ":" + sessionKey
|
||||
if _, loading := al.summarizing.LoadOrStore(summarizeKey, true); !loading {
|
||||
go func() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
||||
)
|
||||
|
|
@ -119,6 +120,26 @@ func (t *ReadFileTool) Parameters() map[string]any {
|
|||
|
||||
const maxReadFileChars = 10000
|
||||
|
||||
func isBinaryContent(data []byte) bool {
|
||||
if len(data) == 0 {
|
||||
return false
|
||||
}
|
||||
sample := data
|
||||
if len(sample) > 512 {
|
||||
sample = sample[:512]
|
||||
}
|
||||
if !utf8.Valid(sample) {
|
||||
return true
|
||||
}
|
||||
nullCount := 0
|
||||
for _, b := range sample {
|
||||
if b == 0 {
|
||||
nullCount++
|
||||
}
|
||||
}
|
||||
return nullCount > 0
|
||||
}
|
||||
|
||||
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
|
|
@ -130,6 +151,10 @@ func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
|||
return ErrorResult(err.Error())
|
||||
}
|
||||
|
||||
if isBinaryContent(content) {
|
||||
return ErrorResult(fmt.Sprintf("binary file detected (%d bytes). Use exec to inspect binary files (e.g. file, xxd, strings)", len(content)))
|
||||
}
|
||||
|
||||
text := string(content)
|
||||
if len(text) > maxReadFileChars {
|
||||
text = text[:maxReadFileChars] + fmt.Sprintf("\n\n... (truncated, showing %d of %d chars. Use exec with head/tail/sed for specific sections)", maxReadFileChars, len(content))
|
||||
|
|
@ -238,14 +263,24 @@ func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolRes
|
|||
return formatDirEntries(entries)
|
||||
}
|
||||
|
||||
const maxDirEntries = 200
|
||||
|
||||
func formatDirEntries(entries []os.DirEntry) *ToolResult {
|
||||
var result strings.Builder
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
result.WriteString("DIR: " + entry.Name() + "\n")
|
||||
} else {
|
||||
result.WriteString("FILE: " + entry.Name() + "\n")
|
||||
total := len(entries)
|
||||
limit := total
|
||||
if limit > maxDirEntries {
|
||||
limit = maxDirEntries
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
if entries[i].IsDir() {
|
||||
result.WriteString("DIR: " + entries[i].Name() + "\n")
|
||||
} else {
|
||||
result.WriteString("FILE: " + entries[i].Name() + "\n")
|
||||
}
|
||||
}
|
||||
if total > maxDirEntries {
|
||||
fmt.Fprintf(&result, "\n... (%d more entries not shown, total: %d)\n", total-maxDirEntries, total)
|
||||
}
|
||||
return NewToolResult(result.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,13 +359,18 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
|||
userContent = userContent[:maxUserLen] + "..."
|
||||
}
|
||||
|
||||
// ForLLM: Full execution details
|
||||
// ForLLM: Execution details (truncated to control token usage)
|
||||
labelStr := label
|
||||
if labelStr == "" {
|
||||
labelStr = "(unnamed)"
|
||||
}
|
||||
resultContent := loopResult.Content
|
||||
const maxSubagentLLM = 3000
|
||||
if len(resultContent) > maxSubagentLLM {
|
||||
resultContent = resultContent[:maxSubagentLLM] + fmt.Sprintf("... (truncated, %d chars total)", len(loopResult.Content))
|
||||
}
|
||||
llmContent := fmt.Sprintf("Subagent task completed:\nLabel: %s\nIterations: %d\nResult: %s",
|
||||
labelStr, loopResult.Iterations, loopResult.Content)
|
||||
labelStr, loopResult.Iterations, resultContent)
|
||||
|
||||
return &ToolResult{
|
||||
ForLLM: llmContent,
|
||||
|
|
|
|||
|
|
@ -661,21 +661,17 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
|||
text = text[:maxChars]
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"url": urlStr,
|
||||
"status": resp.StatusCode,
|
||||
"extractor": extractor,
|
||||
"truncated": truncated,
|
||||
"length": len(text),
|
||||
"text": text,
|
||||
var llmContent string
|
||||
if truncated {
|
||||
llmContent = fmt.Sprintf("[%s %d truncated=%v]\n%s", urlStr, resp.StatusCode, truncated, text)
|
||||
} else {
|
||||
llmContent = fmt.Sprintf("[%s %d]\n%s", urlStr, resp.StatusCode, text)
|
||||
}
|
||||
|
||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||
|
||||
return &ToolResult{
|
||||
ForLLM: string(resultJSON),
|
||||
ForLLM: llmContent,
|
||||
ForUser: fmt.Sprintf(
|
||||
"Fetched %d bytes from %s (extractor: %s, truncated: %v)",
|
||||
"Fetched %d chars from %s (extractor: %s, truncated: %v)",
|
||||
len(text),
|
||||
urlStr,
|
||||
extractor,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue