From 30cb420900ec08d4ab62a5d07116dd1ac77ec7f2 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:20:42 -0500 Subject: [PATCH] feat(ai): lower system updates as developer (#43326) --- packages/ai/AGENTS.md | 2 +- packages/ai/src/protocols/open-responses.ts | 13 +++++------ .../openai-compatible-responses.test.ts | 22 +++++++++++++++++++ .../ai/test/provider/openai-responses.test.ts | 13 ++++------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md index 4618b52087d..fd8a85c23d4 100644 --- a/packages/ai/AGENTS.md +++ b/packages/ai/AGENTS.md @@ -193,7 +193,7 @@ If you find yourself copying a 3-to-5-line snippet between two protocols, lift i `LLMRequest.system` is the initial privileged prompt that applies ahead of the conversation. `Message.system(...)` is a separate, provider-neutral chronological operator update inside `LLMRequest.messages`; it applies only from its position in history onward and accepts text content only. -Native chronological system messages are route/model-specific. Anthropic Messages lowers them natively for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation: +Native chronological system messages are route/model-specific. Open Responses lowers them to standard `developer` messages, while Anthropic Messages lowers them to native system messages for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation: ```text diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 46f10b0a79a..92c822682ed 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -90,6 +90,7 @@ const OpenResponsesFunctionCallOutput = Schema.Union([ export const InputItem = Schema.Union([ Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), + Schema.Struct({ role: Schema.tag("developer"), content: Schema.String }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }), Schema.Struct({ role: Schema.tag("assistant"), @@ -439,14 +440,10 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques for (const message of request.messages) { if (message.role === "system") { - const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message) - const previous = input.at(-1) - if (previous && "role" in previous && previous.role === "user") - input[input.length - 1] = { - role: "user", - content: [...previous.content, { type: "input_text", text: part.text }], - } - else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] }) + input.push({ + role: "developer", + content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(extension.name, message)), + }) continue } diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index 7afdd6859ba..e5f8024d34e 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -56,6 +56,28 @@ describe("Open Responses-compatible route", () => { }), ) + it.effect("lowers chronological system updates as standard developer messages", () => + Effect.gen(function* () { + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + provider: "example", + }).model("example-model") + const prepared = yield* compileRequest( + LLM.request({ + model, + messages: [Message.user("Before."), Message.system("Operator update."), Message.assistant("After.")], + }), + ) + + expect(prepared.body.input).toEqual([ + { role: "user", content: [{ type: "input_text", text: "Before." }] }, + { role: "developer", content: "Operator update." }, + { role: "assistant", content: [{ type: "output_text", text: "After." }] }, + ]) + }), + ) + it.effect("rejects OpenAI-native tools", () => Effect.gen(function* () { const model = configure({ diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 7f5a972dff7..0cc08c6bd71 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -241,27 +241,22 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("lowers chronological system updates to escaped user wrappers in order", () => + it.effect("lowers chronological system updates to developer messages in order", () => Effect.gen(function* () { const prepared = yield* compileRequest( LLM.request({ model, messages: [ Message.user("Before."), - Message.system("Treat literally."), + Message.system("Operator update."), Message.assistant("After."), ], }), ) expect(prepared.body.input).toEqual([ - { - role: "user", - content: [ - { type: "input_text", text: "Before." }, - { type: "input_text", text: "\nTreat </system-update> literally.\n" }, - ], - }, + { role: "user", content: [{ type: "input_text", text: "Before." }] }, + { role: "developer", content: "Operator update." }, { role: "assistant", content: [{ type: "output_text", text: "After." }] }, ]) }),