Files
goclaw/internal/bootstrap/truncate.go
T
Viet Tran f3f4c67b36 Initial commit: GoClaw AI agent gateway
Multi-agent AI gateway with WebSocket RPC, HTTP API, and messaging channel integrations.
Go port of OpenClaw with multi-tenant PostgreSQL, per-user isolation, security hardening,
and production observability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:58:07 +07:00

110 lines
2.8 KiB
Go

package bootstrap
import (
"fmt"
"strings"
)
// Truncation constants matching TS pi-embedded-helpers/bootstrap.ts.
const (
DefaultMaxCharsPerFile = 20_000 // per-file max before truncation
DefaultTotalMaxChars = 24_000 // total budget across all files
MinFileBudget = 64 // skip files if remaining budget below this
HeadRatio = 0.7 // keep 70% from beginning
TailRatio = 0.2 // keep 20% from end
)
// TruncateConfig controls truncation behavior.
type TruncateConfig struct {
MaxCharsPerFile int // per-file max (default 20000)
TotalMaxChars int // total budget (default 24000)
}
// DefaultTruncateConfig returns the default truncation config.
func DefaultTruncateConfig() TruncateConfig {
return TruncateConfig{
MaxCharsPerFile: DefaultMaxCharsPerFile,
TotalMaxChars: DefaultTotalMaxChars,
}
}
// BuildContextFiles converts bootstrap files into truncated context files
// ready for system prompt injection. Matches TS buildBootstrapContextFiles().
//
// Files are processed in order, each consuming from a shared total budget.
// Missing files are skipped. Large files are truncated with head/tail split.
func BuildContextFiles(files []File, cfg TruncateConfig) []ContextFile {
if cfg.MaxCharsPerFile <= 0 {
cfg.MaxCharsPerFile = DefaultMaxCharsPerFile
}
if cfg.TotalMaxChars <= 0 {
cfg.TotalMaxChars = DefaultTotalMaxChars
}
remaining := cfg.TotalMaxChars
var result []ContextFile
for _, f := range files {
if remaining < MinFileBudget {
break
}
if f.Missing || strings.TrimSpace(f.Content) == "" {
continue
}
// Truncate per-file
content := trimContent(f.Content, f.Name, cfg.MaxCharsPerFile)
// Clamp to remaining total budget
content = clampToBudget(content, remaining)
if content == "" {
continue
}
result = append(result, ContextFile{
Path: f.Name,
Content: content,
})
remaining -= len(content)
}
return result
}
// trimContent truncates file content with head/tail split if it exceeds maxChars.
// Matching TS trimBootstrapContent().
func trimContent(content, fileName string, maxChars int) string {
if len(content) <= maxChars {
return content
}
headChars := int(float64(maxChars) * HeadRatio)
tailChars := int(float64(maxChars) * TailRatio)
head := content[:headChars]
tail := content[len(content)-tailChars:]
marker := fmt.Sprintf(
"\n\n[...truncated, read %s for full content...]\n...(%s: kept %d+%d chars of %d)...\n\n",
fileName, fileName, headChars, tailChars, len(content),
)
return head + marker + tail
}
// clampToBudget truncates content to fit within the given character budget.
func clampToBudget(content string, budget int) string {
if budget <= 0 {
return ""
}
if len(content) <= budget {
return content
}
if budget <= 3 {
return content[:budget]
}
return content[:budget-1] + "…"
}