From 04c6bed240ea466bbe70d08ea47394a62d794a4b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 28 Jun 2026 14:02:46 -0400 Subject: [PATCH] feat(core): restore per-request system prompt (#34335) --- packages/client/src/generated/types.ts | 12 +++++ packages/core/src/session.ts | 1 + packages/core/src/session/message-updater.ts | 1 + packages/core/src/session/runner/llm.ts | 5 +- packages/core/test/session-prompt.test.ts | 27 ++++++++++ packages/core/test/session-runner.test.ts | 53 ++++++++++++++++++++ packages/schema/src/prompt-input.ts | 1 + packages/schema/src/prompt.ts | 4 +- packages/schema/src/session-message.ts | 1 + 9 files changed, 103 insertions(+), 2 deletions(-) diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 4251e4f969f..7d6702ca7fa 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -412,6 +412,7 @@ export type SessionsPromptInput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery?: "steer" | "queue" | null readonly resume?: boolean | null @@ -430,6 +431,7 @@ export type SessionsPromptInput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery?: "steer" | "queue" | null readonly resume?: boolean | null @@ -448,6 +450,7 @@ export type SessionsPromptInput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery?: "steer" | "queue" | null readonly resume?: boolean | null @@ -466,6 +469,7 @@ export type SessionsPromptInput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery?: "steer" | "queue" | null readonly resume?: boolean | null @@ -490,6 +494,7 @@ export type SessionsPromptOutput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery: "steer" | "queue" readonly timeCreated: number @@ -569,6 +574,7 @@ export type SessionsContextOutput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string readonly type: "user" } | { @@ -769,6 +775,7 @@ export type SessionsHistoryOutput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery: "steer" | "queue" } @@ -796,6 +803,7 @@ export type SessionsHistoryOutput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery: "steer" | "queue" } @@ -1235,6 +1243,7 @@ export type SessionsEventsOutput = readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery: "steer" | "queue" } @@ -1262,6 +1271,7 @@ export type SessionsEventsOutput = readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string } readonly delivery: "steer" | "queue" } @@ -1663,6 +1673,7 @@ export type SessionsMessageOutput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string readonly type: "user" } | { @@ -1835,6 +1846,7 @@ export type MessagesListOutput = { readonly name: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } }> + readonly system?: string readonly type: "user" } | { diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 26f00c29b87..636f9a535ca 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -492,6 +492,7 @@ const resolvePrompt = (input: PromptInput.Prompt) => Prompt.make({ text: input.text, agents: input.agents, + system: input.system, files: input.files?.map((file) => { const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 3269aa1d728..0cf07fd752f 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -133,6 +133,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { text: event.data.prompt.text, files: event.data.prompt.files, agents: event.data.prompt.agents, + system: event.data.prompt.system, time: { created: event.data.timestamp }, }), ) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index c2834a5f4dc..f9601529858 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -194,13 +194,16 @@ export const layer = Layer.effect( const model = yield* models.resolve(session) const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq) const context = entries.map((entry) => entry.message) + // Mirror V1 (session/llm/request.ts): append the current turn's per-request system string after the + // agent prompt and durable baseline. The current turn's user prompt is the latest user message in context. + const turnSystem = context.findLast((message) => message.type === "user")?.system const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agent.info?.permissions) const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id const request = LLM.request({ model, providerOptions: { openai: { promptCacheKey } }, - system: [agent.info?.system, system.baseline] + system: [agent.info?.system, system.baseline, turnSystem] .filter((part): part is string => part !== undefined && part.length > 0) .map(SystemPart.make), messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])], diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 2bd43226df4..18017e5052f 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -174,6 +174,33 @@ describe("SessionV2.prompt", () => { }), ) + it.effect("preserves an optional per-request system string through admission and projection", () => + Effect.gen(function* () { + yield* setup + const { db } = yield* Database.Service + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + + const message = yield* session.prompt({ + sessionID, + prompt: Prompt.make({ text: "Fix the failing tests", system: "Per-request override" }), + resume: false, + }) + + expect(message.prompt.system).toBe("Per-request override") + expect(yield* admitted(message.id)).toMatchObject({ + id: message.id, + prompt: { text: "Fix the failing tests", system: "Per-request override" }, + }) + + yield* SessionInput.promoteSteers(db, events, sessionID, Number.MAX_SAFE_INTEGER) + + expect(yield* session.messages({ sessionID })).toMatchObject([ + { id: message.id, type: "user", text: "Fix the failing tests", system: "Per-request override" }, + ]) + }), + ) + it.effect("resolves attachment MIME before admission", () => Effect.gen(function* () { yield* setup diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index fb6f5cdc4f4..4d47d652674 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -847,6 +847,59 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("appends the per-request prompt system after the agent prompt and durable baseline", () => + Effect.gen(function* () { + yield* setup + const agent = yield* AgentV2.Service + yield* agent.transform((editor) => + editor.update(AgentV2.ID.make("build"), (agent) => { + agent.system = "Build agent instructions" + agent.mode = "primary" + }), + ) + const session = yield* SessionV2.Service + yield* session.prompt({ + sessionID, + prompt: Prompt.make({ text: "First", system: "Per-request override" }), + resume: false, + }) + + requests.length = 0 + response = fragmentFixture("text", "text-system", ["Done"]).completeEvents + yield* session.resume(sessionID) + + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + "Build agent instructions", + "Initial context", + "Per-request override", + ]) + }), + ) + + it.effect("omits the per-request system part when the prompt has no system string", () => + Effect.gen(function* () { + yield* setup + const agent = yield* AgentV2.Service + yield* agent.transform((editor) => + editor.update(AgentV2.ID.make("build"), (agent) => { + agent.system = "Build agent instructions" + agent.mode = "primary" + }), + ) + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) + + requests.length = 0 + response = fragmentFixture("text", "text-no-system", ["Done"]).completeEvents + yield* session.resume(sessionID) + + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + "Build agent instructions", + "Initial context", + ]) + }), + ) + it.effect("uses an explicitly selected non-build agent system", () => Effect.gen(function* () { yield* setup diff --git a/packages/schema/src/prompt-input.ts b/packages/schema/src/prompt-input.ts index f2a0d460dfd..63829bc8c37 100644 --- a/packages/schema/src/prompt-input.ts +++ b/packages/schema/src/prompt-input.ts @@ -23,4 +23,5 @@ export const Prompt = Schema.Struct({ text: Schema.String, files: Schema.Array(FileAttachment).pipe(optional), agents: Schema.Array(AgentAttachment).pipe(optional), + system: Schema.String.pipe(optional), }).annotate({ identifier: "PromptInput" }) diff --git a/packages/schema/src/prompt.ts b/packages/schema/src/prompt.ts index 376700cc7a6..05f0f793024 100644 --- a/packages/schema/src/prompt.ts +++ b/packages/schema/src/prompt.ts @@ -42,16 +42,18 @@ export const Prompt = Schema.Struct({ text: Schema.String, files: Schema.Array(FileAttachment).pipe(optional), agents: Schema.Array(AgentAttachment).pipe(optional), + system: Schema.String.pipe(optional), }) .annotate({ identifier: "Prompt" }) .pipe( statics((schema) => ({ equivalence: Schema.toEquivalence(schema), - fromUserMessage: (input: Pick) => + fromUserMessage: (input: Pick) => schema.make({ text: input.text, ...(input.files === undefined ? {} : { files: input.files }), ...(input.agents === undefined ? {} : { agents: input.agents }), + ...(input.system === undefined ? {} : { system: input.system }), }), })), ) diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 58ff532063a..3132f303036 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -47,6 +47,7 @@ export const User = Schema.Struct({ text: Prompt.fields.text, files: Prompt.fields.files, agents: Prompt.fields.agents, + system: Prompt.fields.system, type: Schema.Literal("user"), }).annotate({ identifier: "Session.Message.User" })