From 66c05271a1f02b24d33adfe0288d9dbb011ddff5 Mon Sep 17 00:00:00 2001 From: Agnik47 <140933190+Agnik47@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:12:14 +0530 Subject: [PATCH] fix(tools): stop forcing empty instructions on Responses requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Responses middleware always set `instructions` on the forwarded request, even when the caller never passed one and there were no memories to inject. `replaceMemoryContext("", "")` returns `""`, so those requests went out with `instructions: ""`. Sending the field at all tells the Responses API to drop the instructions carried over from a `previous_response_id` turn, so an empty string silently wipes the caller's system prompt mid-conversation instead of leaving it in place. The no-input early return in the same function already gets this right — it spreads `instructions` only when the caller supplied a string. Apply the same rule to the main path, while still injecting when there are memories to add. Adds three regression tests to the existing OpenAI middleware unit suite, which CI already runs on changes to packages/tools. --- packages/tools/src/openai/middleware.ts | 11 ++- .../tools/test/openai-middleware.unit.test.ts | 98 +++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/openai/middleware.ts b/packages/tools/src/openai/middleware.ts index 518db547a..4dde209fc 100644 --- a/packages/tools/src/openai/middleware.ts +++ b/packages/tools/src/openai/middleware.ts @@ -970,13 +970,22 @@ export function createOpenAIMiddleware( : "" } + // Sending `instructions` at all tells the Responses API to drop whatever a + // `previous_response_id` turn carried over, so an empty result must leave + // the caller's own field untouched rather than blank it. Matches the + // no-input early return above. + const instructionsOverride = + enhancedInstructions || typeof params.instructions === "string" + ? { instructions: enhancedInstructions } + : {} + return { request: originalResponsesCreate.call( openaiClient.responses, { ...params, input: cleanedInput, - instructions: enhancedInstructions, + ...instructionsOverride, }, requestOptions, ), diff --git a/packages/tools/test/openai-middleware.unit.test.ts b/packages/tools/test/openai-middleware.unit.test.ts index e3249c8d3..bdd882139 100644 --- a/packages/tools/test/openai-middleware.unit.test.ts +++ b/packages/tools/test/openai-middleware.unit.test.ts @@ -63,3 +63,101 @@ describe("OpenAI middleware memory context", () => { ).toHaveLength(1) }) }) + +describe("OpenAI Responses middleware instructions", () => { + const originalApiKey = process.env.SUPERMEMORY_API_KEY + + beforeEach(() => { + process.env.SUPERMEMORY_API_KEY = "sm_test_key" + }) + + afterEach(() => { + if (originalApiKey === undefined) delete process.env.SUPERMEMORY_API_KEY + else process.env.SUPERMEMORY_API_KEY = originalApiKey + vi.unstubAllGlobals() + }) + + const stubMemoryFetch = (staticMemories: string[]) => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + profile: { + static: staticMemories.map((memory) => ({ memory })), + dynamic: [], + }, + searchResults: { results: [] }, + }), + }), + ) + } + + const wrapResponsesClient = () => { + const originalCreate = vi.fn(() => + Object.assign(Promise.resolve({ output: [] }), { + asResponse: async () => new Response(), + }), + ) + const client = { + chat: { completions: { create: vi.fn() } }, + responses: { create: originalCreate }, + } as unknown as OpenAI + const wrapped = withSupermemory(client, { + containerTag: "user-a", + customId: "conversation-a", + mode: "profile", + addMemory: "never", + }) + return { originalCreate, wrapped } + } + + it("leaves instructions absent when the caller sent none and there is nothing to inject", async () => { + stubMemoryFetch([]) + const { originalCreate, wrapped } = wrapResponsesClient() + + await wrapped.responses.create({ + model: "gpt-4o-mini", + input: "What do you remember?", + previous_response_id: "resp_123", + }) + + // An empty `instructions` still counts as present, and the Responses API + // drops the previous turn's instructions whenever the field is sent. + const forwarded = originalCreate.mock.calls[0]?.[0] + expect("instructions" in forwarded).toBe(false) + expect(forwarded.previous_response_id).toBe("resp_123") + }) + + it("still injects memories as instructions when the caller sent none", async () => { + stubMemoryFetch(["Fresh profile fact"]) + const { originalCreate, wrapped } = wrapResponsesClient() + + await wrapped.responses.create({ + model: "gpt-4o-mini", + input: "What do you remember?", + }) + + const forwarded = originalCreate.mock.calls[0]?.[0] + expect(String(forwarded.instructions)).toContain("Fresh profile fact") + }) + + it("keeps caller instructions and strips stale context when there is nothing to inject", async () => { + stubMemoryFetch([]) + const { originalCreate, wrapped } = wrapResponsesClient() + + await wrapped.responses.create({ + model: "gpt-4o-mini", + input: "What do you remember?", + instructions: [ + "Be helpful.", + '', + "Stale profile fact", + "", + ].join("\n"), + }) + + const forwarded = originalCreate.mock.calls[0]?.[0] + expect(String(forwarded.instructions)).toBe("Be helpful.") + }) +})