Просмотр исходного кода

feat(core): restore per-request system prompt (#34335)

Kit Langton 1 месяц назад
Родитель
Сommit
04c6bed240

+ 12 - 0
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"
       }
     | {

+ 1 - 0
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)

+ 1 - 0
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 },
           }),
         )

+ 4 - 1
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)] : [])],

+ 27 - 0
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

+ 53 - 0
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

+ 1 - 0
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" })

+ 3 - 1
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<Prompt, "text" | "files" | "agents">) =>
+      fromUserMessage: (input: Pick<Prompt, "text" | "files" | "agents" | "system">) =>
         schema.make({
           text: input.text,
           ...(input.files === undefined ? {} : { files: input.files }),
           ...(input.agents === undefined ? {} : { agents: input.agents }),
+          ...(input.system === undefined ? {} : { system: input.system }),
         }),
     })),
   )

+ 1 - 0
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" })