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
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxBootstrapFileChars = 4096
|
||||||
|
|
||||||
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||||
bootstrapFiles := []string{
|
bootstrapFiles := []string{
|
||||||
"AGENTS.md",
|
"AGENTS.md",
|
||||||
|
|
@ -345,7 +347,11 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||||
for _, filename := range bootstrapFiles {
|
for _, filename := range bootstrapFiles {
|
||||||
filePath := filepath.Join(cb.workspace, filename)
|
filePath := filepath.Join(cb.workspace, filename)
|
||||||
if data, err := os.ReadFile(filePath); err == nil {
|
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)
|
messages = append(messages, toolResultMsg)
|
||||||
|
|
||||||
// Save tool result message to session
|
// Save a truncated version of tool results to session history.
|
||||||
agent.Sessions.AddFullMessage(opts.SessionKey, toolResultMsg)
|
// 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)
|
tokenEstimate := al.estimateTokens(newHistory)
|
||||||
threshold := agent.ContextWindow * 75 / 100
|
threshold := agent.ContextWindow * 75 / 100
|
||||||
|
|
||||||
if len(newHistory) > 20 || tokenEstimate > threshold {
|
if len(newHistory) > 12 || tokenEstimate > threshold {
|
||||||
summarizeKey := agent.ID + ":" + sessionKey
|
summarizeKey := agent.ID + ":" + sessionKey
|
||||||
if _, loading := al.summarizing.LoadOrStore(summarizeKey, true); !loading {
|
if _, loading := al.summarizing.LoadOrStore(summarizeKey, true); !loading {
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
"github.com/sipeed/picoclaw/pkg/fileutil"
|
||||||
)
|
)
|
||||||
|
|
@ -119,6 +120,26 @@ func (t *ReadFileTool) Parameters() map[string]any {
|
||||||
|
|
||||||
const maxReadFileChars = 10000
|
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 {
|
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -130,6 +151,10 @@ func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
return ErrorResult(err.Error())
|
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)
|
text := string(content)
|
||||||
if len(text) > maxReadFileChars {
|
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))
|
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,15 +263,25 @@ func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolRes
|
||||||
return formatDirEntries(entries)
|
return formatDirEntries(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxDirEntries = 200
|
||||||
|
|
||||||
func formatDirEntries(entries []os.DirEntry) *ToolResult {
|
func formatDirEntries(entries []os.DirEntry) *ToolResult {
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
for _, entry := range entries {
|
total := len(entries)
|
||||||
if entry.IsDir() {
|
limit := total
|
||||||
result.WriteString("DIR: " + entry.Name() + "\n")
|
if limit > maxDirEntries {
|
||||||
|
limit = maxDirEntries
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
if entries[i].IsDir() {
|
||||||
|
result.WriteString("DIR: " + entries[i].Name() + "\n")
|
||||||
} else {
|
} else {
|
||||||
result.WriteString("FILE: " + entry.Name() + "\n")
|
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())
|
return NewToolResult(result.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -359,13 +359,18 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
userContent = userContent[:maxUserLen] + "..."
|
userContent = userContent[:maxUserLen] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForLLM: Full execution details
|
// ForLLM: Execution details (truncated to control token usage)
|
||||||
labelStr := label
|
labelStr := label
|
||||||
if labelStr == "" {
|
if labelStr == "" {
|
||||||
labelStr = "(unnamed)"
|
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",
|
llmContent := fmt.Sprintf("Subagent task completed:\nLabel: %s\nIterations: %d\nResult: %s",
|
||||||
labelStr, loopResult.Iterations, loopResult.Content)
|
labelStr, loopResult.Iterations, resultContent)
|
||||||
|
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: llmContent,
|
ForLLM: llmContent,
|
||||||
|
|
|
||||||
|
|
@ -661,21 +661,17 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
text = text[:maxChars]
|
text = text[:maxChars]
|
||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]any{
|
var llmContent string
|
||||||
"url": urlStr,
|
if truncated {
|
||||||
"status": resp.StatusCode,
|
llmContent = fmt.Sprintf("[%s %d truncated=%v]\n%s", urlStr, resp.StatusCode, truncated, text)
|
||||||
"extractor": extractor,
|
} else {
|
||||||
"truncated": truncated,
|
llmContent = fmt.Sprintf("[%s %d]\n%s", urlStr, resp.StatusCode, text)
|
||||||
"length": len(text),
|
|
||||||
"text": text,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
|
||||||
|
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: string(resultJSON),
|
ForLLM: llmContent,
|
||||||
ForUser: fmt.Sprintf(
|
ForUser: fmt.Sprintf(
|
||||||
"Fetched %d bytes from %s (extractor: %s, truncated: %v)",
|
"Fetched %d chars from %s (extractor: %s, truncated: %v)",
|
||||||
len(text),
|
len(text),
|
||||||
urlStr,
|
urlStr,
|
||||||
extractor,
|
extractor,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue