Skip to content

Commit 1a7c835

Browse files
adam.wilsoncursoragent
andcommitted
feat(web): apply chat preferences to agent system prompt
Threads the signed-in user's chat preferences through the agent so the final answer is biased by their saved settings. - `agent.ts` exports a new `ResolvedChatUserPreferences` interface and accepts it as an optional field on `CreateMessageStreamResponseProps`, `AgentOptions`, and `createPrompt`. When present, `renderChatPreferencesPromptBlock()` produces a `<user_preferences>` block that is appended after `</answer_instructions>` in the system prompt, where the model sees it last. When absent, no block is emitted and behavior is identical to today. - `api/(server)/chat/route.ts` loads `chatPreferences` and `chatCustomInstructions` from the user's row (only when `user` is defined, which excludes anonymous chats), parses the JSONB through `chatPreferencesSchema` defensively, and passes the result to `createMessageStream`. The query is `select`-narrowed to the two columns the agent actually needs. - `features/mcp/askCodebase.ts` does not load preferences. MCP has no user-settings surface yet; the call site has an explicit comment pointing back to #1242 so a future maintainer knows it was a conscious omission rather than an oversight. Soft-bias framing in the prompt block is intentional: preferences are described as guidance that shapes the final answer and must never override correctness or the explicit user message. This keeps existing agent behavior intact even when preferences are aggressive (e.g. "one-paragraph summary" + "describe code only" together). Refs #1242 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9d40e7d commit 1a7c835

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

packages/web/src/app/api/(server)/chat/route.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { sew } from "@/middleware/sew";
2-
import { createMessageStream } from "@/features/chat/agent";
2+
import { createMessageStream, ResolvedChatUserPreferences } from "@/features/chat/agent";
33
import { additionalChatRequestParamsSchema } from "@/features/chat/types";
44
import { getLanguageModelKey } from "@/features/chat/utils";
55
import { getAISDKLanguageModelAndOptions, getConfiguredLanguageModels, isOwnerOfChat, updateChatMessages } from "@/features/chat/utils.server";
6+
import { chatPreferencesSchema } from "@/features/chat/userPreferences";
67
import { apiHandler } from "@/lib/apiHandler";
78
import { ErrorCode } from "@/lib/errorCodes";
89
import { captureEvent } from "@/lib/posthog";
@@ -93,6 +94,27 @@ export const POST = apiHandler(async (req: NextRequest) => {
9394

9495
const source = req.headers.get('X-Sourcebot-Client-Source') ?? undefined;
9596

97+
// Load the user's chat-style preferences. Anonymous users skip this
98+
// entirely so the agent uses default behavior with no
99+
// `<user_preferences>` block in the system prompt.
100+
let userPreferences: ResolvedChatUserPreferences | undefined;
101+
if (user) {
102+
const row = await prisma.user.findUnique({
103+
where: { id: user.id },
104+
select: {
105+
chatPreferences: true,
106+
chatCustomInstructions: true,
107+
},
108+
});
109+
if (row) {
110+
const parsed = chatPreferencesSchema.safeParse(row.chatPreferences);
111+
userPreferences = {
112+
preferences: parsed.success ? parsed.data : {},
113+
customInstructions: row.chatCustomInstructions,
114+
};
115+
}
116+
}
117+
96118
await captureEvent('ask_message_sent', {
97119
chatId: id,
98120
messageCount: messages.length,
@@ -112,6 +134,7 @@ export const POST = apiHandler(async (req: NextRequest) => {
112134
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
113135
modelProviderOptions: providerOptions,
114136
modelTemperature: temperature,
137+
userPreferences,
115138
onFinish: async ({ messages }) => {
116139
await updateChatMessages({ chatId: id, messages, prisma });
117140
},

packages/web/src/features/chat/agent.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ import { ANSWER_TAG, FILE_REFERENCE_PREFIX } from "./constants";
1717
import { Source } from "./types";
1818
import { addLineNumbers, fileReferenceToString } from "./utils";
1919
import { createTools } from "./tools";
20+
import {
21+
ChatPreferences,
22+
renderChatPreferencesPromptBlock,
23+
} from "./userPreferences";
24+
25+
/**
26+
* Resolved chat-style preferences for the user who is sending this message.
27+
* `undefined` means no signed-in user (or no preferences loaded) and the
28+
* agent uses default behavior with no `<user_preferences>` block.
29+
*/
30+
export interface ResolvedChatUserPreferences {
31+
preferences: ChatPreferences;
32+
customInstructions: string | null;
33+
}
2034

2135
const dedent = _dedent.withOptions({ alignValues: true });
2236

@@ -43,6 +57,12 @@ interface CreateMessageStreamResponseProps {
4357
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
4458
modelTemperature?: number;
4559
metadata?: Partial<SBChatMessageMetadata>;
60+
/**
61+
* Per-user response-style preferences. When omitted (e.g. anonymous user
62+
* or MCP caller), the agent uses default behavior with no
63+
* `<user_preferences>` block injected into the system prompt.
64+
*/
65+
userPreferences?: ResolvedChatUserPreferences;
4666
}
4767

4868
export const createMessageStream = async ({
@@ -56,6 +76,7 @@ export const createMessageStream = async ({
5676
modelTemperature,
5777
onFinish,
5878
onError,
79+
userPreferences,
5980
}: CreateMessageStreamResponseProps) => {
6081
const latestMessage = messages[messages.length - 1];
6182
const sources = latestMessage.parts
@@ -109,6 +130,7 @@ export const createMessageStream = async ({
109130
},
110131
traceId,
111132
chatId,
133+
userPreferences,
112134
});
113135

114136
await mergeStreamAsync(researchStream, writer, {
@@ -154,6 +176,7 @@ interface AgentOptions {
154176
onWriteSource: (source: Source) => void;
155177
traceId: string;
156178
chatId: string;
179+
userPreferences?: ResolvedChatUserPreferences;
157180
}
158181

159182
const createAgentStream = async ({
@@ -166,6 +189,7 @@ const createAgentStream = async ({
166189
onWriteSource,
167190
traceId,
168191
chatId,
192+
userPreferences,
169193
}: AgentOptions) => {
170194
// For every file source, resolve the source code so that we can include it in the system prompt.
171195
const fileSources = inputSources.filter((source) => source.type === 'file');
@@ -195,6 +219,7 @@ const createAgentStream = async ({
195219
const systemPrompt = createPrompt({
196220
repos: selectedRepos,
197221
files: resolvedFileSources,
222+
userPreferences,
198223
});
199224

200225
const stream = streamText({
@@ -234,6 +259,7 @@ const createAgentStream = async ({
234259
const createPrompt = ({
235260
files,
236261
repos,
262+
userPreferences,
237263
}: {
238264
files?: {
239265
path: string;
@@ -243,7 +269,12 @@ const createPrompt = ({
243269
revision: string;
244270
}[],
245271
repos: string[],
272+
userPreferences?: ResolvedChatUserPreferences,
246273
}) => {
274+
const preferencesBlock = userPreferences
275+
? renderChatPreferencesPromptBlock(userPreferences)
276+
: null;
277+
247278
return dedent`
248279
You are a powerful agentic AI code assistant built into Sourcebot, the world's best code-intelligence platform. Your job is to help developers understand and navigate their large codebases.
249280
@@ -317,6 +348,8 @@ const createPrompt = ({
317348
Authentication in Sourcebot is built on NextAuth.js with a session-based approach using JWT tokens and Prisma as the database adapter ${fileReferenceToString({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts', range: { startLine: 135, endLine: 140 } })}. The system supports multiple authentication providers and implements organization-based authorization with role-defined permissions.
318349
\`\`\`
319350
</answer_instructions>
351+
352+
${preferencesBlock ?? ''}
320353
`
321354
}
322355

packages/web/src/features/mcp/askCodebase.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
148148
} : {}),
149149
});
150150

151+
// `userPreferences` is intentionally omitted here. Chat preferences
152+
// are a web-app UI feature and there is no MCP-side surface to set
153+
// or override them today. Revisit if/when MCP grows a user-settings
154+
// story (tracked in #1242).
151155
const stream = await createMessageStream({
152156
chatId: chat.id,
153157
messages: [userMessage],

0 commit comments

Comments
 (0)