on main help sign in
perf: use strings.Builder for string concatenation in loops (#1819)
0868681Carlos Alexandro Becker
internal/agent/agent.go+16−9
@@ -561,15 +561,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
561 return err
561 return err
562 }
562 }
563
563
564− summaryPromptText := "Provide a detailed summary of our conversation above."
565− if len(currentSession.Todos) > 0 {
564+ summaryPromptText := buildSummaryPrompt(currentSession.Todos)
566− summaryPromptText += "\n\n## Current Todo List\n\n"
567− for _, t := range currentSession.Todos {
568− summaryPromptText += fmt.Sprintf("- [%s] %s\n", t.Status, t.Content)
569− }
570− summaryPromptText += "\nInclude these tasks and their statuses in your summary. "
571− summaryPromptText += "Instruct the resuming assistant to use the `todos` tool to continue tracking progress on these tasks."
572− }
573
565
574 resp, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
566 resp, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
575 Prompt: summaryPromptText,
567 Prompt: summaryPromptText,
@@ -1101,3 +1093,18 @@ func (a *sessionAgent) workaroundProviderMediaLimitations(messages []fantasy.Mes
1101
1093
1102 return convertedMessages
1094 return convertedMessages
1103}
1095}
1096+
1097+// buildSummaryPrompt constructs the prompt text for session summarization.
1098+func buildSummaryPrompt(todos []session.Todo) string {
1099+ var sb strings.Builder
1100+ sb.WriteString("Provide a detailed summary of our conversation above.")
1101+ if len(todos) > 0 {
1102+ sb.WriteString("\n\n## Current Todo List\n\n")
1103+ for _, t := range todos {
1104+ fmt.Fprintf(&sb, "- [%s] %s\n", t.Status, t.Content)
1105+ }
1106+ sb.WriteString("\nInclude these tasks and their statuses in your summary. ")
1107+ sb.WriteString("Instruct the resuming assistant to use the `todos` tool to continue tracking progress on these tasks.")
1108+ }
1109+ return sb.String()
1110+}
internal/agent/agent_test.go+36
@@ -1,6 +1,7 @@
1package agent
1package agent
2
2
3import (
3import (
4+ "fmt"
4 "os"
5 "os"
5 "path/filepath"
6 "path/filepath"
6 "runtime"
7 "runtime"
@@ -11,6 +12,7 @@ import (
11 "charm.land/x/vcr"
12 "charm.land/x/vcr"
12 "github.com/charmbracelet/crush/internal/agent/tools"
13 "github.com/charmbracelet/crush/internal/agent/tools"
13 "github.com/charmbracelet/crush/internal/message"
14 "github.com/charmbracelet/crush/internal/message"
15+ "github.com/charmbracelet/crush/internal/session"
14 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
17 "github.com/stretchr/testify/require"
16
18
@@ -619,3 +621,37 @@ func TestCoderAgent(t *testing.T) {
619 })
621 })
620 }
622 }
621}
623}
624+
625+func makeTestTodos(n int) []session.Todo {
626+ todos := make([]session.Todo, n)
627+ for i := range n {
628+ todos[i] = session.Todo{
629+ Status: session.TodoStatusPending,
630+ Content: fmt.Sprintf("Task %d: Implement feature with some description that makes it realistic", i),
631+ }
632+ }
633+ return todos
634+}
635+
636+func BenchmarkBuildSummaryPrompt(b *testing.B) {
637+ cases := []struct {
638+ name string
639+ numTodos int
640+ }{
641+ {"0todos", 0},
642+ {"5todos", 5},
643+ {"10todos", 10},
644+ {"50todos", 50},
645+ }
646+
647+ for _, tc := range cases {
648+ todos := makeTestTodos(tc.numTodos)
649+
650+ b.Run(tc.name, func(b *testing.B) {
651+ b.ReportAllocs()
652+ for range b.N {
653+ _ = buildSummaryPrompt(todos)
654+ }
655+ })
656+ }
657+}
internal/message/content.go+10−6
@@ -437,23 +437,27 @@ func (m *Message) AddBinary(mimeType string, data []byte) {
437}
437}
438
438
439func PromptWithTextAttachments(prompt string, attachments []Attachment) string {
439func PromptWithTextAttachments(prompt string, attachments []Attachment) string {
440+ var sb strings.Builder
441+ sb.WriteString(prompt)
440 addedAttachments := false
442 addedAttachments := false
441 for _, content := range attachments {
443 for _, content := range attachments {
442 if !content.IsText() {
444 if !content.IsText() {
443 continue
445 continue
444 }
446 }
445 if !addedAttachments {
447 if !addedAttachments {
446− prompt += "\n<system_info>The files below have been attached by the user, consider them in your response</system_info>\n"
448+ sb.WriteString("\n<system_info>The files below have been attached by the user, consider them in your response</system_info>\n")
447 addedAttachments = true
449 addedAttachments = true
448 }
450 }
449− tag := `<file>\n`
450 if content.FilePath != "" {
451 if content.FilePath != "" {
451− tag = fmt.Sprintf("<file path='%s'>\n", content.FilePath)
452+ fmt.Fprintf(&sb, "<file path='%s'>\n", content.FilePath)
453+ } else {
454+ sb.WriteString("<file>\n")
452 }
455 }
453− prompt += tag
456+ sb.WriteString("\n")
454− prompt += "\n" + string(content.Content) + "\n</file>\n"
457+ sb.Write(content.Content)
458+ sb.WriteString("\n</file>\n")
455 }
459 }
456− return prompt
460+ return sb.String()
457}
461}
458
462
459func (m *Message) ToAIMessage() []fantasy.Message {
463func (m *Message) ToAIMessage() []fantasy.Message {
internal/message/content_test.gonew file+45
@@ -0,0 +1,45 @@
1+package message
2+
3+import (
4+ "fmt"
5+ "strings"
6+ "testing"
7+)
8+
9+func makeTestAttachments(n int, contentSize int) []Attachment {
10+ attachments := make([]Attachment, n)
11+ content := []byte(strings.Repeat("x", contentSize))
12+ for i := range n {
13+ attachments[i] = Attachment{
14+ FilePath: fmt.Sprintf("/path/to/file%d.txt", i),
15+ MimeType: "text/plain",
16+ Content: content,
17+ }
18+ }
19+ return attachments
20+}
21+
22+func BenchmarkPromptWithTextAttachments(b *testing.B) {
23+ cases := []struct {
24+ name string
25+ numFiles int
26+ contentSize int
27+ }{
28+ {"1file_100bytes", 1, 100},
29+ {"5files_1KB", 5, 1024},
30+ {"10files_10KB", 10, 10 * 1024},
31+ {"20files_50KB", 20, 50 * 1024},
32+ }
33+
34+ for _, tc := range cases {
35+ attachments := makeTestAttachments(tc.numFiles, tc.contentSize)
36+ prompt := "Process these files"
37+
38+ b.Run(tc.name, func(b *testing.B) {
39+ b.ReportAllocs()
40+ for range b.N {
41+ _ = PromptWithTextAttachments(prompt, attachments)
42+ }
43+ })
44+ }
45+}