mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-11 18:12:09 +00:00
f3f4c67b36
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>
110 lines
2.8 KiB
Go
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] + "…"
|
|
}
|