fix(coderd/x/chatd): preserve quickgen user input

This commit is contained in:
Michael Suchacz
2026-05-28 16:39:09 +00:00
parent dbe1c28663
commit 5713a39d9a
2 changed files with 10 additions and 33 deletions
-10
View File
@@ -424,10 +424,6 @@ func (p *Server) prepareQuickgenDebugCandidate(
return runCtx, debugModel, finishDebugRun return runCtx, debugModel, finishDebugRun
} }
// Synthetic quickgen prompts end with an assistant marker because AI Bridge
// records final user-role messages as user prompts.
const quickgenStructuredOutputReady = "Ready to provide the structured output."
func syntheticObjectGenerationPrompt(systemPrompt, userInput string) fantasy.Prompt { func syntheticObjectGenerationPrompt(systemPrompt, userInput string) fantasy.Prompt {
return fantasy.Prompt{ return fantasy.Prompt{
{ {
@@ -442,12 +438,6 @@ func syntheticObjectGenerationPrompt(systemPrompt, userInput string) fantasy.Pro
fantasy.TextPart{Text: userInput}, fantasy.TextPart{Text: userInput},
}, },
}, },
{
Role: fantasy.MessageRoleAssistant,
Content: []fantasy.MessagePart{
fantasy.TextPart{Text: quickgenStructuredOutputReady},
},
},
} }
} }
+10 -23
View File
@@ -417,7 +417,7 @@ func TestMaybeGenerateChatTitlePreservesUpdatedAt(t *testing.T) {
model := &chattest.FakeModel{ model := &chattest.FakeModel{
GenerateObjectFn: func(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) { GenerateObjectFn: func(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
require.Equal(t, "propose_title", call.SchemaName) require.Equal(t, "propose_title", call.SchemaName)
requireSyntheticQuickgenContext(ctx, t) require.True(t, suppressAIBridgeSessionHeadersFromContext(ctx))
requireSyntheticQuickgenPrompt(t, call.Prompt, userPrompt) requireSyntheticQuickgenPrompt(t, call.Prompt, userPrompt)
return &fantasy.ObjectResponse{ return &fantasy.ObjectResponse{
Object: map[string]any{"title": wantTitle}, Object: map[string]any{"title": wantTitle},
@@ -496,7 +496,7 @@ func Test_generateManualTitle_UsesTimeout(t *testing.T) {
deadline, deadline,
2*time.Second, 2*time.Second,
) )
requireSyntheticQuickgenContext(ctx, t) require.True(t, suppressAIBridgeSessionHeadersFromContext(ctx))
requireSyntheticQuickgenPrompt(t, call.Prompt, "refresh chat title") requireSyntheticQuickgenPrompt(t, call.Prompt, "refresh chat title")
require.Equal(t, "propose_title", call.SchemaName) require.Equal(t, "propose_title", call.SchemaName)
return &fantasy.ObjectResponse{Object: map[string]any{"title": "Refresh title"}}, nil return &fantasy.ObjectResponse{Object: map[string]any{"title": "Refresh title"}}, nil
@@ -527,7 +527,7 @@ func Test_generateManualTitle_TruncatesFirstUserInput(t *testing.T) {
model := &chattest.FakeModel{ model := &chattest.FakeModel{
GenerateObjectFn: func(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) { GenerateObjectFn: func(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
requireSyntheticQuickgenContext(ctx, t) require.True(t, suppressAIBridgeSessionHeadersFromContext(ctx))
requireSyntheticQuickgenPrompt(t, call.Prompt, truncateRunes(longFirstUserText, maxLatestUserMessageRunes)) requireSyntheticQuickgenPrompt(t, call.Prompt, truncateRunes(longFirstUserText, maxLatestUserMessageRunes))
// The manual title system prompt also includes the latest user excerpt. // The manual title system prompt also includes the latest user excerpt.
systemText, ok := call.Prompt[0].Content[0].(fantasy.TextPart) systemText, ok := call.Prompt[0].Content[0].(fantasy.TextPart)
@@ -689,7 +689,7 @@ func TestGenerateStructuredTitleWithUsage_OpenAICompatibleRequiredToolChoice(t *
body := testutil.TryReceive(t.Context(), t, requests) body := testutil.TryReceive(t.Context(), t, requests)
require.Equal(t, "required", body["tool_choice"]) require.Equal(t, "required", body["tool_choice"])
requireOpenAICompatAssistantFinalMessage(t, body) requireOpenAICompatFinalUserMessage(t, body, "summarize failed workspace build logs")
} }
func newOpenAICompatStructuredOutputServer( func newOpenAICompatStructuredOutputServer(
@@ -767,33 +767,20 @@ func openAICompatTestModel(t *testing.T, baseURL string) fantasy.LanguageModel {
return model return model
} }
func requireSyntheticQuickgenContext(ctx context.Context, t *testing.T) {
t.Helper()
require.True(t, suppressAIBridgeSessionHeadersFromContext(ctx))
}
func requireSyntheticQuickgenPrompt(t *testing.T, prompt fantasy.Prompt, userInput string) { func requireSyntheticQuickgenPrompt(t *testing.T, prompt fantasy.Prompt, userInput string) {
t.Helper() t.Helper()
require.Len(t, prompt, 3) require.Len(t, prompt, 2)
require.Equal(t, fantasy.MessageRoleSystem, prompt[0].Role) require.Equal(t, fantasy.MessageRoleSystem, prompt[0].Role)
require.Equal(t, fantasy.MessageRoleUser, prompt[1].Role) require.Equal(t, fantasy.MessageRoleUser, prompt[1].Role)
require.Equal(t, fantasy.MessageRoleAssistant, prompt[2].Role)
require.Len(t, prompt[1].Content, 1) require.Len(t, prompt[1].Content, 1)
require.Len(t, prompt[2].Content, 1)
userText, ok := prompt[1].Content[0].(fantasy.TextPart) userText, ok := prompt[1].Content[0].(fantasy.TextPart)
require.True(t, ok) require.True(t, ok)
require.Equal(t, userInput, userText.Text) require.Equal(t, userInput, userText.Text)
assistantText, ok := prompt[2].Content[0].(fantasy.TextPart)
require.True(t, ok)
require.Equal(t, quickgenStructuredOutputReady, assistantText.Text)
} }
func requireOpenAICompatAssistantFinalMessage(t *testing.T, body map[string]any) { func requireOpenAICompatFinalUserMessage(t *testing.T, body map[string]any, userInput string) {
t.Helper() t.Helper()
messages, ok := body["messages"].([]any) messages, ok := body["messages"].([]any)
@@ -802,8 +789,8 @@ func requireOpenAICompatAssistantFinalMessage(t *testing.T, body map[string]any)
lastMessage, ok := messages[len(messages)-1].(map[string]any) lastMessage, ok := messages[len(messages)-1].(map[string]any)
require.True(t, ok) require.True(t, ok)
require.Equal(t, "assistant", lastMessage["role"]) require.Equal(t, "user", lastMessage["role"])
require.Equal(t, quickgenStructuredOutputReady, lastMessage["content"]) require.Equal(t, userInput, lastMessage["content"])
} }
func TestGenerateStructuredTurnStatusLabel(t *testing.T) { func TestGenerateStructuredTurnStatusLabel(t *testing.T) {
@@ -815,7 +802,7 @@ func TestGenerateStructuredTurnStatusLabel(t *testing.T) {
model := &chattest.FakeModel{ model := &chattest.FakeModel{
GenerateObjectFn: func(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) { GenerateObjectFn: func(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
require.Equal(t, "propose_turn_status_label", call.SchemaName) require.Equal(t, "propose_turn_status_label", call.SchemaName)
requireSyntheticQuickgenContext(ctx, t) require.True(t, suppressAIBridgeSessionHeadersFromContext(ctx))
requireSyntheticQuickgenPrompt(t, call.Prompt, "done") requireSyntheticQuickgenPrompt(t, call.Prompt, "done")
return &fantasy.ObjectResponse{ return &fantasy.ObjectResponse{
Object: map[string]any{"label": "Submitted PR"}, Object: map[string]any{"label": "Submitted PR"},
@@ -841,7 +828,7 @@ func TestGenerateStructuredTurnStatusLabel(t *testing.T) {
body := testutil.TryReceive(t.Context(), t, requests) body := testutil.TryReceive(t.Context(), t, requests)
require.Equal(t, "required", body["tool_choice"]) require.Equal(t, "required", body["tool_choice"])
requireOpenAICompatAssistantFinalMessage(t, body) requireOpenAICompatFinalUserMessage(t, body, "done")
}) })
t.Run("rejects narrative label", func(t *testing.T) { t.Run("rejects narrative label", func(t *testing.T) {