diff --git a/agent.go b/agent.go index 6ec571c3b0eccdc04dddf5c2470c3a0618edd054..08d488770354ef3c1af6685bba1bd04d4e453533 100644 --- a/agent.go +++ b/agent.go @@ -11,6 +11,7 @@ import ( "sync" "charm.land/fantasy/schema" + "github.com/charmbracelet/x/exp/slice" ) // StepResult represents the result of a single step in an agent execution. @@ -1019,8 +1020,33 @@ func (a *agent) validateToolCall(toolCall ToolCallContent, availableTools []Agen } func (a *agent) createPrompt(system, prompt string, messages []Message, files ...FilePart) (Prompt, error) { + // Validation: empty prompt is only allowed when there are messages, + // no files to attach, and the last message is a user or tool message. if prompt == "" { - return nil, &Error{Title: "invalid argument", Message: "prompt can't be empty"} + lastMessage, hasMessages := slice.Last(messages) + + if !hasMessages { + return nil, &Error{ + Title: "invalid argument", + Message: "prompt can't be empty when there are no messages", + } + } + + if len(files) > 0 { + return nil, &Error{ + Title: "invalid argument", + Message: "prompt can't be empty when there are files", + } + } + + switch lastMessage.Role { + case MessageRoleUser, MessageRoleTool: + default: + return nil, &Error{ + Title: "invalid argument", + Message: "prompt can't be empty when the last message is not a user or tool message", + } + } } var preparedPrompt Prompt @@ -1029,7 +1055,9 @@ func (a *agent) createPrompt(system, prompt string, messages []Message, files .. preparedPrompt = append(preparedPrompt, NewSystemMessage(system)) } preparedPrompt = append(preparedPrompt, messages...) - preparedPrompt = append(preparedPrompt, NewUserMessage(prompt, files...)) + if prompt != "" { + preparedPrompt = append(preparedPrompt, NewUserMessage(prompt, files...)) + } return preparedPrompt, nil } diff --git a/agent_test.go b/agent_test.go index f6df57987d124b65ba7bb6b4964eddd7cc5ee188..929094b21899bb14f8a519e8d52a29e29040a355 100644 --- a/agent_test.go +++ b/agent_test.go @@ -523,20 +523,91 @@ func TestAgent_Generate_BasicText(t *testing.T) { require.Equal(t, int64(13), result.TotalUsage.TotalTokens) } -// Test empty prompt error +// Test empty prompt validation func TestAgent_Generate_EmptyPrompt(t *testing.T) { t.Parallel() model := &mockLanguageModel{} agent := NewAgent(model) - result, err := agent.Generate(context.Background(), AgentCall{ - Prompt: "", // Empty prompt should cause error + t.Run("fails without messages", func(t *testing.T) { + result, err := agent.Generate(context.Background(), AgentCall{ + Prompt: "", + }) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "prompt can't be empty when there are no messages") + }) + + t.Run("fails with files even if messages exist", func(t *testing.T) { + result, err := agent.Generate(context.Background(), AgentCall{ + Prompt: "", + Messages: []Message{ + {Role: MessageRoleUser, Content: []MessagePart{TextPart{Text: "hello"}}}, + }, + Files: []FilePart{{Filename: "test.txt", Data: []byte("test"), MediaType: "text/plain"}}, + }) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "prompt can't be empty when there are files") + }) + + t.Run("fails when last message is assistant", func(t *testing.T) { + result, err := agent.Generate(context.Background(), AgentCall{ + Prompt: "", + Messages: []Message{ + {Role: MessageRoleUser, Content: []MessagePart{TextPart{Text: "hello"}}}, + {Role: MessageRoleAssistant, Content: []MessagePart{TextPart{Text: "hi there"}}}, + }, + }) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "prompt can't be empty when the last message is not a user or tool message") + }) + + t.Run("succeeds when last message is user", func(t *testing.T) { + model := &mockLanguageModel{ + generateFunc: func(ctx context.Context, call Call) (*Response, error) { + return &Response{ + Content: []Content{TextContent{Text: "response"}}, + FinishReason: FinishReasonStop, + }, nil + }, + } + agent := NewAgent(model) + + result, err := agent.Generate(context.Background(), AgentCall{ + Prompt: "", + Messages: []Message{ + {Role: MessageRoleUser, Content: []MessagePart{TextPart{Text: "hello"}}}, + }, + }) + require.NoError(t, err) + require.NotNil(t, result) }) - require.Error(t, err) - require.Nil(t, result) - require.Contains(t, err.Error(), "invalid argument: prompt can't be empty") + t.Run("succeeds when last message is tool", func(t *testing.T) { + model := &mockLanguageModel{ + generateFunc: func(ctx context.Context, call Call) (*Response, error) { + return &Response{ + Content: []Content{TextContent{Text: "response"}}, + FinishReason: FinishReasonStop, + }, nil + }, + } + agent := NewAgent(model) + + result, err := agent.Generate(context.Background(), AgentCall{ + Prompt: "", + Messages: []Message{ + {Role: MessageRoleUser, Content: []MessagePart{TextPart{Text: "hello"}}}, + {Role: MessageRoleAssistant, Content: []MessagePart{ToolCallPart{ToolCallID: "call_1", ToolName: "test"}}}, + {Role: MessageRoleTool, Content: []MessagePart{ToolResultPart{ToolCallID: "call_1", Output: ToolResultOutputContentText{Text: "result"}}}}, + }, + }) + require.NoError(t, err) + require.NotNil(t, result) + }) } // Test with system prompt