diff --git a/internal/agent/agent.go b/internal/agent/agent.go
index 7c7ac4c6c1f3d320fe3e3dd865f8e7b56c73010d..dae219a53f5387e45dcb084fa6ac5ae4a7165a4b 100644
--- a/internal/agent/agent.go
+++ b/internal/agent/agent.go
@@ -561,15 +561,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
return err
}
- summaryPromptText := "Provide a detailed summary of our conversation above."
- if len(currentSession.Todos) > 0 {
- summaryPromptText += "\n\n## Current Todo List\n\n"
- for _, t := range currentSession.Todos {
- summaryPromptText += fmt.Sprintf("- [%s] %s\n", t.Status, t.Content)
- }
- summaryPromptText += "\nInclude these tasks and their statuses in your summary. "
- summaryPromptText += "Instruct the resuming assistant to use the `todos` tool to continue tracking progress on these tasks."
- }
+ summaryPromptText := buildSummaryPrompt(currentSession.Todos)
resp, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
Prompt: summaryPromptText,
@@ -1101,3 +1093,18 @@ func (a *sessionAgent) workaroundProviderMediaLimitations(messages []fantasy.Mes
return convertedMessages
}
+
+// buildSummaryPrompt constructs the prompt text for session summarization.
+func buildSummaryPrompt(todos []session.Todo) string {
+ var sb strings.Builder
+ sb.WriteString("Provide a detailed summary of our conversation above.")
+ if len(todos) > 0 {
+ sb.WriteString("\n\n## Current Todo List\n\n")
+ for _, t := range todos {
+ fmt.Fprintf(&sb, "- [%s] %s\n", t.Status, t.Content)
+ }
+ sb.WriteString("\nInclude these tasks and their statuses in your summary. ")
+ sb.WriteString("Instruct the resuming assistant to use the `todos` tool to continue tracking progress on these tasks.")
+ }
+ return sb.String()
+}
diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go
index ca5bb10dca1bc1096429e99dc217389d30e90248..38a154d8c449b2cc148ef5038d7facf49450bbb3 100644
--- a/internal/agent/agent_test.go
+++ b/internal/agent/agent_test.go
@@ -1,6 +1,7 @@
package agent
import (
+ "fmt"
"os"
"path/filepath"
"runtime"
@@ -11,6 +12,7 @@ import (
"charm.land/x/vcr"
"github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/message"
+ "github.com/charmbracelet/crush/internal/session"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -619,3 +621,37 @@ func TestCoderAgent(t *testing.T) {
})
}
}
+
+func makeTestTodos(n int) []session.Todo {
+ todos := make([]session.Todo, n)
+ for i := range n {
+ todos[i] = session.Todo{
+ Status: session.TodoStatusPending,
+ Content: fmt.Sprintf("Task %d: Implement feature with some description that makes it realistic", i),
+ }
+ }
+ return todos
+}
+
+func BenchmarkBuildSummaryPrompt(b *testing.B) {
+ cases := []struct {
+ name string
+ numTodos int
+ }{
+ {"0todos", 0},
+ {"5todos", 5},
+ {"10todos", 10},
+ {"50todos", 50},
+ }
+
+ for _, tc := range cases {
+ todos := makeTestTodos(tc.numTodos)
+
+ b.Run(tc.name, func(b *testing.B) {
+ b.ReportAllocs()
+ for range b.N {
+ _ = buildSummaryPrompt(todos)
+ }
+ })
+ }
+}
diff --git a/internal/message/content.go b/internal/message/content.go
index 6c03d42aed05a7772f37d15dc782bf96c8b69685..3fed1f06019c855d30af9d5583e6a7b63fcbd508 100644
--- a/internal/message/content.go
+++ b/internal/message/content.go
@@ -437,23 +437,27 @@ func (m *Message) AddBinary(mimeType string, data []byte) {
}
func PromptWithTextAttachments(prompt string, attachments []Attachment) string {
+ var sb strings.Builder
+ sb.WriteString(prompt)
addedAttachments := false
for _, content := range attachments {
if !content.IsText() {
continue
}
if !addedAttachments {
- prompt += "\nThe files below have been attached by the user, consider them in your response\n"
+ sb.WriteString("\nThe files below have been attached by the user, consider them in your response\n")
addedAttachments = true
}
- tag := `\n`
if content.FilePath != "" {
- tag = fmt.Sprintf("\n", content.FilePath)
+ fmt.Fprintf(&sb, "\n", content.FilePath)
+ } else {
+ sb.WriteString("\n")
}
- prompt += tag
- prompt += "\n" + string(content.Content) + "\n\n"
+ sb.WriteString("\n")
+ sb.Write(content.Content)
+ sb.WriteString("\n\n")
}
- return prompt
+ return sb.String()
}
func (m *Message) ToAIMessage() []fantasy.Message {
diff --git a/internal/message/content_test.go b/internal/message/content_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7e9e273c57e4b6cee2df8cd6b74bf455797bce36
--- /dev/null
+++ b/internal/message/content_test.go
@@ -0,0 +1,45 @@
+package message
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func makeTestAttachments(n int, contentSize int) []Attachment {
+ attachments := make([]Attachment, n)
+ content := []byte(strings.Repeat("x", contentSize))
+ for i := range n {
+ attachments[i] = Attachment{
+ FilePath: fmt.Sprintf("/path/to/file%d.txt", i),
+ MimeType: "text/plain",
+ Content: content,
+ }
+ }
+ return attachments
+}
+
+func BenchmarkPromptWithTextAttachments(b *testing.B) {
+ cases := []struct {
+ name string
+ numFiles int
+ contentSize int
+ }{
+ {"1file_100bytes", 1, 100},
+ {"5files_1KB", 5, 1024},
+ {"10files_10KB", 10, 10 * 1024},
+ {"20files_50KB", 20, 50 * 1024},
+ }
+
+ for _, tc := range cases {
+ attachments := makeTestAttachments(tc.numFiles, tc.contentSize)
+ prompt := "Process these files"
+
+ b.Run(tc.name, func(b *testing.B) {
+ b.ReportAllocs()
+ for range b.N {
+ _ = PromptWithTextAttachments(prompt, attachments)
+ }
+ })
+ }
+}