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." }] },
])
}),