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) + } + }) + } +}