# OpenAI Responses API memory: conversation state and beyond

> Does the Responses API remember earlier turns? How previous_response_id, store and the Conversations API keep state, and how to add long-term memory to it.

**Short answer:** the Responses API remembers earlier turns only if you ask it to, with `previous_response_id` or a `conversation`. Otherwise each call sees only the `input` it carries. And when it does carry state, it carries the whole conversation: every earlier input token is billed again on each call, and nothing is ever let go.

This guide covers how the Responses API keeps state, what it does with reasoning and tool calls, what changed with the end of the Assistants API, and how to replace chaining with a memory: each call sends only what is new, and the request that comes back holds what the model should see.

## Does the Responses API remember earlier turns?

Not by default. There are three ways to give the next call the earlier turns, all from OpenAI's [conversation state guide](https://developers.openai.com/api/docs/guides/conversation-state):

- **Send them yourself.** Append each response's `output` items to your `input` array and send the whole array next time.
- **Chain with `previous_response_id`.** Name the last response and send only the new input. OpenAI looks up the stored chain.
- **Attach a `conversation`.** Items from the conversation are prepended to the input, and the response's input and output items are added to the conversation when it completes ([API reference](https://developers.openai.com/api/reference/resources/responses/methods/create)).

Chaining, in TypeScript:

```ts
import OpenAI from 'openai';

const openai = new OpenAI();

const first = await openai.responses.create({ model: 'gpt-5', input: 'Tell me a joke.' });
const second = await openai.responses.create({
  model: 'gpt-5',
  previous_response_id: first.id,
  input: [{ role: 'user', content: 'Explain why it is funny.' }],
});
console.log(second.output_text);
```

The same in Python, and with a conversation instead of a chain:

```python
from openai import OpenAI

client = OpenAI()

first = client.responses.create(model="gpt-5", input="Tell me a joke.")
second = client.responses.create(
    model="gpt-5",
    previous_response_id=first.id,
    input=[{"role": "user", "content": "Explain why it is funny."}],
)

conversation = client.conversations.create()
reply = client.responses.create(
    model="gpt-5",
    conversation=conversation.id,
    input=[{"role": "user", "content": "Tell me a joke."}],
)
```

And over HTTP:

```bash
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"model":"gpt-5","previous_response_id":"resp_123","input":[{"role":"user","content":"Explain why it is funny."}]}'
```

`previous_response_id` and `conversation` cannot be used together in one request ([API reference](https://developers.openai.com/api/reference/resources/responses/methods/create)).

## Ways to carry state, side by side

| | Manual `input` array | `previous_response_id` | Conversations API | Threadless AI |
|---|---|---|---|---|
| Who stores the history | You | OpenAI, as stored responses | OpenAI, as conversation items | Us, per memory |
| How long it is kept | As long as you keep it | 30 days by default | Until deleted | Until you delete the memory |
| Input billed per call | The whole array | All earlier input tokens in the chain | Every item in the conversation | What the returned request holds, which stops growing |
| What is let go | Whatever you cut | Nothing | Nothing | Older messages, word for word, once their facts are noted; they come back when the conversation turns to them |
| Works with `store: false` / ZDR | Yes | Only by replaying items instead | No | Yes: `store` is dropped from the request |

Retention and billing are from OpenAI's [conversation state guide](https://developers.openai.com/api/docs/guides/conversation-state) and [data controls](https://developers.openai.com/api/docs/guides/your-data). With `store: false`, OpenAI's [programmatic tool calling guide](https://developers.openai.com/api/docs/guides/tools-programmatic-tool-calling) says to replay the complete item sequence instead of continuing from `previous_response_id`.

## Are instructions carried over with previous_response_id?

No. The reference for `instructions` says: "When using along with `previous_response_id`, the instructions from a previous response will not be carried over to the next response" ([API reference](https://developers.openai.com/api/reference/resources/responses/methods/create)). Send `instructions` on every call. It also means you can change them mid-conversation.

## Reasoning items and tool calls across turns

- **Reasoning items are encrypted by default.** "The Responses API returns encrypted reasoning items by default." For stateless requests, OpenAI says to "preserve every item in the response's `output` array" when you replay it ([conversation state](https://developers.openai.com/api/docs/guides/conversation-state)).
- **`include: ["reasoning.encrypted_content"]`** returns an encrypted version of the reasoning tokens in reasoning items, so you can carry them forward without storage ([API reference](https://developers.openai.com/api/reference/resources/responses/methods/create)).
- **Tool results go back as `function_call_output` items** with the `call_id` of the call they answer. Keep each `function_call` and its output together: the model expects both.

With Threadless AI, send the output items exactly as returned. Reasoning items are stored as sent and come back in the same format.

## What happens after 30 days?

Stored responses are kept "for at least 30 days" by default ([API reference](https://developers.openai.com/api/reference/resources/responses/methods/create)), so a conversation that sits idle beyond that can lose the chain it depends on. Do not build long-lived conversations on `previous_response_id` alone. Conversation items have no 30-day TTL ([conversation state](https://developers.openai.com/api/docs/guides/conversation-state)), and a memory keeps its history until you delete it.

## Migrating from the Assistants API

The Assistants API is gone. OpenAI announced its deprecation on 26 August 2025 and removed it on 26 August 2026; the replacement is the Responses API and the Conversations API ([Deprecations](https://developers.openai.com/api/docs/deprecations)). OpenAI's [Assistants migration guide](https://developers.openai.com/api/docs/assistants/migration) maps the concepts:

| Assistants API | Now |
|---|---|
| Assistants | Prompts (configuration: model, tools, instructions) |
| Threads | Conversations |
| Runs | Responses |
| Run steps | Items |

Points to know before you move:

- **Threads can no longer be read from the API.** The migration guide says the call that retrieves thread messages no longer works; rebuild history from messages your application stored.
- **Prompt objects are going too.** OpenAI's [deprecations page](https://developers.openai.com/api/docs/deprecations) schedules the `v1/prompts` API and reusable prompt objects to shut down on 30 November 2026, and says to move prompt content into application code. Put your old assistant's instructions in `instructions`.
- **Conversations take up to 20 items per call** when created or appended to ([Conversations reference](https://developers.openai.com/api/reference/resources/conversations)). Import long histories in batches.
- **Threads were a log, not a memory.** Moving a thread into a conversation keeps the same property: every item is sent on every call.

If you are rebuilding anyway, you can skip the conversation: create a memory, send the stored messages as the first call's new items, and continue from there with the loop below.

## Add long-term memory: install and create a memory

```bash
npm install @threadlessai/sdk     # or: pip install threadlessai
export THREADLESS_API_KEY=tl_test_...
```

Create one memory per conversation and store its id next to yours.

```ts
import { Threadless } from '@threadlessai/sdk';

const memory = new Threadless();
const { id: memoryId } = await memory.memories.create({ metadata: { conversation: 'c_123' } });
```

## The loop, in TypeScript

```ts
import OpenAI from 'openai';
import { isNoteCall, NOTE_RESULT, Threadless } from '@threadlessai/sdk';

const openai = new OpenAI();
const memory = new Threadless();
const tools = [memory.responses.noteTool, ...yourTools];

export async function send(memoryId: string, text: string) {
  let newItems: Array<Record<string, unknown>> = [{ role: 'user', content: text }];
  for (;;) {
    const request = await memory.responses.decorate(memoryId, newItems, { model: 'gpt-5', tools });
    const reply = await openai.responses.create(
      request as OpenAI.Responses.ResponseCreateParamsNonStreaming,
    );
    memory.responses.report(memoryId, reply);

    const calls = reply.output.filter((item) => item.type === 'function_call');
    if (calls.length === 0) return reply.output_text;
    newItems = reply.output.map((item) => ({ ...item }));
    for (const call of calls) {
      const output = isNoteCall(call.name) ? NOTE_RESULT : await runTool(call);
      newItems.push({ type: 'function_call_output', call_id: call.call_id, output });
    }
  }
}
```

## The loop, in Python

```python
from openai import OpenAI
from threadlessai import NOTE_RESULT, Threadless, is_note_call

openai = OpenAI()
memory = Threadless()
tools = [memory.responses.note_tool, *your_tools]

def send(memory_id: str, text: str):
    new_items = [{"role": "user", "content": text}]
    while True:
        request = memory.responses.decorate(memory_id, new_items, {"model": "gpt-5", "tools": tools})
        reply = openai.responses.create(**request)
        memory.responses.report(memory_id, reply)

        calls = [item for item in reply.output if item.type == "function_call"]
        if not calls:
            return reply.output_text
        new_items = [item.model_dump(exclude_none=True) for item in reply.output]
        for call in calls:
            output = NOTE_RESULT if is_note_call(call.name) else run_tool(call)
            new_items.append({"type": "function_call_output", "call_id": call.call_id, "output": output})
```

## What changes from a stateful Responses agent

- **Drop `previous_response_id` and `store`.** If you pass them, they are removed from the request, because we hold the history. Everything else in the request (`model`, `tools`, `reasoning`, `instructions`, `tool_choice`) passes through unchanged.
- **Send only what is new.** The first call: your developer message and the user's message. A new turn: the model's last output and the user's message. Inside a tool loop: the output items and your `function_call_output` items. Output you already reported is recognised and not stored twice.
- **Call `decorate` before every model call**, including each call inside a tool loop.
- **Report every reply**, exactly as returned, `usage` included. The usage lets us correct our size estimate for your model.

## What the request holds

The `input` that comes back has your developer message first, then a facts block of what was learned from older messages, then the latest messages word for word, with a recall block before a user message that touches something old. When older messages are about to stop being sent word for word, the request ends with a note ask; the model answers it with one call to the note tool, and your loop answers that call with `noted`.

Token use stays bounded: the request stops growing with the conversation ([How it works](https://threadlessai.com/how-it-works#bounded)).

## Streaming

If you stream, report the final response once the stream ends: the `response` of the `response.completed` event. See [Add memory to a streaming chatbot](https://threadlessai.com/guides/streaming-chatbot-memory).

## Questions people ask

### Is the Responses API stateful?

It can be. Responses are stored by default and you can chain to them or attach a conversation, but a call with neither sees only its own `input`. The state it keeps is the full transcript, billed again on every call.

### Does store: false break previous_response_id?

Over HTTP, yes in practice: with `store: false` OpenAI's guidance is to replay the full item sequence rather than continue from `previous_response_id`. Zero Data Retention forces `store` to `false`.

### How long does OpenAI keep responses?

Thirty days by default for stored responses; `store: false` turns storage off. Conversations and their items are kept until you delete them.

### Can I use previous_response_id with my own memory?

Not with Threadless AI: we drop `previous_response_id` and `store` from the decorated request, because the request already holds what the model should see. Chaining on top would make OpenAI prepend the whole stored chain again.

### Does it work with streaming?

Yes. Stream the request `decorate` returns as you normally would, and report the final response from the `response.completed` event.

### What replaced Assistants API threads?

Conversations. OpenAI removed the Assistants API on 26 August 2026 and maps threads to conversations, runs to responses and run steps to items.

## Related

- [previous_response_id and the Conversations API: what they cost](https://threadlessai.com/guides/previous-response-id-and-conversations-api)
- [OpenAI compaction vs a memory layer](https://threadlessai.com/guides/openai-compaction-vs-memory)
- [Let the model write its own notes](https://threadlessai.com/guides/note-tool-pattern)
- [Models and formats](https://threadlessai.com/docs/models-and-formats)
