OpenCodeReview version
Bug description
OpenAIResponsesClient forces store: false (stateless, by design), which makes the client fully responsible for resending conversation state. But the next request's input is rebuilt from the normalized projection only — assistant text plus function calls. Typed reasoning output items, including encrypted_content, item IDs, and original output ordering, are dropped on every tool turn.
OpenAI's guidance for manually managed history is to preserve and resend the response's output items; for store: false / Zero Data Retention flows, encrypted reasoning items must be replayed. Rebuilding only a message and function calls loses reasoning continuity — and via the same Content() fallback described in #805, the reasoning summary additionally leaks into visible assistant text.
Source trace
All references are main @ 71d2981:
buildResponsesParams rebuilds assistant history as an EasyInputMessage plus function-call items; no code path emits a reasoning input item.
Store: false means manual replay is the only continuation mechanism available.
mapResponsesResponse keeps only the reasoning summary text; encrypted_content and item identity are dropped at parse time.
Deterministic reproduction
No live endpoint or API key needed — this exercises the adapter's own response→history→request chain:
func TestResponsesToolCallReplayPreservesReasoningItems(t *testing.T) {
raw := `{
"id": "resp_01",
"object": "response",
"status": "completed",
"model": "gpt-5.2",
"output": [
{"id": "rs_01", "type": "reasoning", "summary": [{"type": "summary_text", "text": "private plan"}], "encrypted_content": "gAAAA-encrypted-reasoning-state"},
{"id": "fc_01", "type": "function_call", "call_id": "call_01", "name": "file_read", "arguments": "{\"path\":\"main.go\"}", "status": "completed"}
]
}`
var sdkResp responses.Response
if err := json.Unmarshal([]byte(raw), &sdkResp); err != nil {
t.Fatal(err)
}
client := NewOpenAIResponsesClient(ClientConfig{URL: "https://api.openai.com/v1/responses"})
resp := client.mapResponsesResponse(&sdkResp)
// Same response-to-history conversion used by llmloop.Runner.
history := NewToolCallMessage(resp.Content(), resp.ToolCalls())
params := client.buildResponsesParams("gpt-5.2", ChatRequest{
Messages: []Message{
{Role: "user", Content: "review this file"},
history,
NewToolResultMessage("call_01", "package main"),
},
})
payload, err := json.Marshal(params.Input)
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(payload, []byte(`"type":"reasoning"`)) || !bytes.Contains(payload, []byte("gAAAA-encrypted-reasoning-state")) {
t.Fatalf("Responses replay input dropped the typed reasoning item: %s", payload)
}
}
Run:
go test ./internal/llm -run '^TestResponsesToolCallReplayPreservesReasoningItems$' -count=1
Actual result on main @ 71d2981:
--- FAIL: TestResponsesToolCallReplayPreservesReasoningItems (0.00s)
Responses replay input dropped the typed reasoning item: [{"content":"review this file","role":"user"},{"content":"private plan","role":"assistant"},{"arguments":"{\"path\":\"main.go\"}","call_id":"call_01","name":"file_read","type":"function_call"},{"call_id":"call_01","output":"package main","type":"function_call_output"}]
The follow-up input contains no reasoning item at all; the reasoning summary ("private plan") came back as ordinary visible assistant text, and the encrypted state and item IDs are gone.
Expected behavior
The follow-up request's input should include the original typed output items — the reasoning item with its encrypted_content and ID, and the function_call items with their call_ids, in original order — followed by the matched function_call_output items.
Official guidance: https://developers.openai.com/api/docs/guides/latest-model#using-gpt-56
Proposed direction
An adapter-owned openAIResponsesReplay envelope behind the same private replayEnvelope seam discussed in #805/#806, not a reuse of the Chat Completions envelope:
- retain the typed response output items required as subsequent
input, including reasoning and function-call items in original order with their IDs;
- request and preserve
reasoning.encrypted_content whenever the active storage policy requires it;
- use the official SDK's typed input/output conversions where they preserve the wire contract, with narrowly scoped raw preservation only for unsupported provider fields;
- stay opaque to the review loop, normalized
ChatResponse, persistence, and logs;
- participate in cloning, token accounting, and compression through the existing seam.
previous_response_id could later be supported as an explicit, mutually exclusive server-stored continuation mode; it is not a substitute under the current store: false policy.
Key regression coverage for any fix: a strict two-request fake Responses endpoint verifying the original typed output items and matched function_call_output items with exact IDs and ordering; parallel function calls; the store: false encrypted-reasoning round trip; log/persistence tests proving encrypted state is never exposed.
OpenCodeReview version
main:71d29810be1e7f7b183c021fe9b06a2e850f1037protocol: openai-responses)Bug description
OpenAIResponsesClientforcesstore: false(stateless, by design), which makes the client fully responsible for resending conversation state. But the next request's input is rebuilt from the normalized projection only — assistant text plus function calls. Typedreasoningoutput items, includingencrypted_content, item IDs, and original output ordering, are dropped on every tool turn.OpenAI's guidance for manually managed history is to preserve and resend the response's output items; for
store: false/ Zero Data Retention flows, encrypted reasoning items must be replayed. Rebuilding only a message and function calls loses reasoning continuity — and via the sameContent()fallback described in #805, the reasoning summary additionally leaks into visible assistant text.Source trace
All references are
main@71d2981:buildResponsesParamsrebuilds assistant history as anEasyInputMessageplus function-call items; no code path emits a reasoning input item.Store: falsemeans manual replay is the only continuation mechanism available.mapResponsesResponsekeeps only the reasoning summary text;encrypted_contentand item identity are dropped at parse time.Deterministic reproduction
No live endpoint or API key needed — this exercises the adapter's own response→history→request chain:
Run:
Actual result on
main@71d2981:The follow-up input contains no reasoning item at all; the reasoning summary (
"private plan") came back as ordinary visible assistant text, and the encrypted state and item IDs are gone.Expected behavior
The follow-up request's input should include the original typed output items — the
reasoningitem with itsencrypted_contentand ID, and thefunction_callitems with theircall_ids, in original order — followed by the matchedfunction_call_outputitems.Official guidance: https://developers.openai.com/api/docs/guides/latest-model#using-gpt-56
Proposed direction
An adapter-owned
openAIResponsesReplayenvelope behind the same privatereplayEnvelopeseam discussed in #805/#806, not a reuse of the Chat Completions envelope:input, including reasoning and function-call items in original order with their IDs;reasoning.encrypted_contentwhenever the active storage policy requires it;ChatResponse, persistence, and logs;previous_response_idcould later be supported as an explicit, mutually exclusive server-stored continuation mode; it is not a substitute under the currentstore: falsepolicy.Key regression coverage for any fix: a strict two-request fake Responses endpoint verifying the original typed output items and matched
function_call_outputitems with exact IDs and ordering; parallel function calls; thestore: falseencrypted-reasoning round trip; log/persistence tests proving encrypted state is never exposed.