Skip to main content

Part 4: Multi-Turn Conversations with Sessions

At the end of Part 3, the agent asked which operating system your laptop runs — and then could never hear your answer. In this part it learns to remember: one small change in the API, and the agent follows a conversation from the first question to the last.

This is Part 4 of a 20-part series.

  1. Part 1: Overview, Architecture and Azure Setup
  2. Part 2: Create a Production-Grade .NET Solution
  3. Part 3: Your First Agent API with Microsoft Foundry
  4. Part 4: Multi-Turn Conversations with Sessions (you are here)
  5. Part 5: A Plug-and-Play Next.js Chat UI (coming soon)
  6. Part 6: Secure the API with Microsoft Entra ID (coming soon)
  7. Part 7: Give the Agent Enterprise Tools (coming soon)
  8. Part 8: Create Tickets and Test Your Tools (coming soon)
  9. Part 9: Connect an MCP Server (coming soon)
  10. Part 10: Human Approval for Sensitive Actions (coming soon)
  11. Part 11: Approval Cards in the Chat UI (coming soon)
  12. Part 12: Persistent Conversations (coming soon)
  13. Part 13: Memory That Knows the Employee (coming soon)
  14. Part 14: Guardrails — Keep Secrets Out of the Model (coming soon)
  15. Part 15: Audit Logs and Observability (coming soon)
  16. Part 16: Ticket Triage Workflow (coming soon)
  17. Part 17: Manager Approvals and Checkpoints (coming soon)
  18. Part 18: Agent Harness — Weekly Problem Report (coming soon)
  19. Part 19: Deploy to Azure Container Apps (coming soon)
  20. Part 20: Bring It to Microsoft Teams (coming soon)

By the end of this part you will hold a three-turn conversation with the agent from your .http file — a Wi-Fi problem, your answer to its question, and a follow-up — and you will know where that conversation is stored. Every step works in VS Code and in Visual Studio — where the two differ, the page shows a tab for each. Every output on this page is real, captured while building the project.

What You Will Build​

Only two files change, both in the API:

  • Program.cs gets a session store — in Development only — so the API remembers each conversation between requests.
  • Dewiride.ServiceDesk.Api.http gets two follow-up requests that continue the Wi-Fi conversation from Part 3.

This is the journey of a follow-up message once you are done:

The library — the brain from Part 3 — does not change at all. Remembering conversations is a job for the host.

How Conversations Work​

Three ideas carry this part. Each gets a few plain sentences here, and each has a standalone guide with the full story.

Threads and runs​

AG-UI talks about threads and runs. A thread is one conversation, and its threadId names it. A run is one turn: you send a message, the agent answers. Every run has its own runId, and a follow-up run names the run it continues in parentRunId. A conversation with three turns is one thread with three runs:

TurnthreadIdrunIdparentRunIdYou send
1thread-2run-1—The Wi-Fi question
2thread-2run-2run-1"It's a Windows 11 laptop."
3thread-2run-3run-2"I tried all of that, and it still won't connect."

Each request carries only the new message. The server, not the client, remembers what was said before. How to Continue an AG-UI Conversation with threadId and parentRunId explains every field.

Sessions and the session store​

In Microsoft Agent Framework, the state of one conversation is an agent session (AgentSession). For every request, MapAGUIServer asks a session store for the session that belongs to the request's threadId — or a new one, if there is none — runs the agent with it, and saves it back once the answer has finished streaming.

Part 3 registered no store, so every request got a brand-new session. That is why the agent could never use your answer. This part registers the simplest store there is, which keeps sessions in the API's memory. How to Keep Conversation History in a Microsoft Agent Framework AG-UI Server covers stores in depth.

Where the history lives​

The session does not hold the conversation itself. Foundry's Responses API stores every answer — for 30 days by default — under an ID such as resp_0be46b1a…. The session holds the ID of the latest answer rather than the messages. On the next turn, the agent sends Foundry the new message together with that ID, and Foundry puts the conversation back together.

You will see the proof in Step 5. After the second turn, I asked Foundry which input it had used for that answer. The API had sent a single message — It's a Windows 11 laptop. — yet Foundry's list holds the whole conversation:

What Foundry used for the second answer
Item Role Text
--------- --------- --------------------------------------------------------------
message user My laptop won't connect to the office Wi-Fi. What should I do?
reasoning
message assistant Which operating system is your laptop using: Windows or macOS?
message user It's a Windows 11 laptop.

The reasoning row is the model's private thinking from the first turn, kept so it can build on it. How to See the Conversation History Microsoft Foundry Stored for a Response shows how to ask Foundry for this list.

What This Part Costs​

Only model usage. No Azure resources are created.

A conversation costs a little more with every turn, because the model reads everything that came before. The three turns in Steps 5 and 6 used 254, 327 and 600 input tokens. At gpt-5.6-luna's Global Standard prices of $0.20 per million input tokens and $1.20 per million output tokens, the whole three-turn conversation cost about $0.0007, and every request on this page together about a tenth of a cent.

Prerequisites​

  • The solution from Part 3, with its .http file. If you skipped it, follow Part 3 first — this part changes its files.
  • The Azure CLI, signed in to the account with the Foundry User role (Part 3, Step 1).
  • Your editor from Part 2 — VS Code with the C# Dev Kit and REST Client extensions, or Visual Studio 2026 (see Part 2's prerequisites).

This part puts four techniques to work. Each has its own guide, if you want the details:

Every command on this page works exactly as shown in Terminal on macOS, in any Linux shell, in PowerShell on Windows and in your editor's terminal. Run them from the solution root — the DewirideServiceDesk folder.

Step 1: Watch the Agent Forget​

Before you change anything, see the problem for yourself. Start the API from Part 3, exactly as before:

In VS Code's terminal, from the solution root, run:

Terminal
dotnet run --project src/Dewiride.ServiceDesk.Api

Open src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.http and find the Wi-Fi request from Part 3 — the one with "threadId": "thread-2". Add this request directly below it, above the pizza request:

Dewiride.ServiceDesk.Api.http (add below the Wi-Fi request)
# Answer the agent's question in the same conversation: the same threadId, a new runId,
# and parentRunId set to the run it continues. Send only the new message.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-2",
"runId": "run-2",
"parentRunId": "run-1",
"messages": [
{
"id": "message-2",
"role": "user",
"content": "It's a Windows 11 laptop."
}
]
}

###

Compared with the Wi-Fi request above it:

PartValueWhy
"threadId"thread-2, the sameIt is the same conversation
"runId"run-2, newThis is the second turn
"parentRunId"run-1The turn this one follows
"messages"only message-2Just the new message — nothing from the first turn

Now send the two requests, in order: first the Wi-Fi request, then the new one.

Select the Send Request link above each request's POST line. Wait for the first response before you send the second.

The Wi-Fi request answers with the clarifying question you met in Part 3:

The answer to run-1, put together
Which operating system is your laptop using—Windows or macOS?

Here is the response to the new request, shortened as in Part 3 — long messageId values cut short, each rawEvent replaced with {…}, most of the middle left out:

Response to run-2 (shortened)
HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Thu, 29 Oct 2026 16:15:44 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Pragma: no-cache
Transfer-Encoding: chunked

data: {"type":"RUN_STARTED","threadId":"thread-2","runId":"run-2","parentRunId":"run-1"}

data: {"type":"REASONING_ENCRYPTED_VALUE","subtype":"message","entityId":"msg_U9vQL9Y8k2NcVH32Gfa1tcg9","encryptedValue":"gAAAAABq…","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_START","messageId":"msg_0594a82f…","role":"assistant","name":"service-desk","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_0594a82f…","delta":"What","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_0594a82f…","delta":" issue","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_0594a82f…","delta":" are","rawEvent":{…}}

… 8 more TEXT_MESSAGE_CONTENT events …

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_0594a82f…","delta":"?","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_END","messageId":"msg_0594a82f…"}

data: {"type":"RUN_FINISHED","threadId":"thread-2","runId":"run-2","outcome":{"type":"success"},"usage":[{"model":"gpt-5.6-luna-2026-07-09","inputTokens":246,"outputTokens":37,"totalTokens":283,"reasoningTokens":19,"cachedInputTokens":0}]}

Put together, the answer is:

The answer to run-2, put together
What issue are you having with the Windows 11 laptop?

The agent has forgotten the Wi-Fi problem. Two values in the response show why:

  • RUN_STARTED repeats "parentRunId":"run-1". The API read your request correctly — the request is not the problem.
  • RUN_FINISHED reports "inputTokens":246 — fewer than the 254 of the Wi-Fi question. The model received the instructions and your five words, and nothing else. In Part 3's API, every request starts a brand-new session.

Stop the API: press Ctrl+C in VS Code's terminal, or in Visual Studio's console window.

Step 2: Add a Session Store for Development​

Open src/Dewiride.ServiceDesk.Api/Program.cs — the file from Part 3. The technique comes from How to Keep Conversation History in a Microsoft Agent Framework AG-UI Server; here is what it looks like in this API.

1. Add one using line below the first one, using Dewiride.ServiceDesk.Agent;:

Program.cs (add below the first using line)
using Microsoft.Agents.AI.Hosting;

It brings in WithInMemorySessionStore, which comes with the hosting package you installed in Part 3.

2. Register the store. Below the builder.Services.AddAGUIServer(); line, add:

Program.cs (add below AddAGUIServer)
// On your machine only: remember each conversation by its AG-UI threadId, in memory.
// Part 6 ties every conversation to a signed-in user; Part 12 stores them durably.
if (builder.Environment.IsDevelopment())
{
serviceDeskAgent.WithInMemorySessionStore(withIsolation: false);
}
CodeWhat it does
builder.Environment.IsDevelopment()true only when the API runs in the Development environment — on your machine, where the launch profile from Part 2 sets ASPNETCORE_ENVIRONMENT to Development. See How to Run Code Only in Development in ASP.NET Core
serviceDeskAgentThe handle that AddServiceDeskAgent() returned in Part 3. As Part 3 promised, conversation storage attaches here
WithInMemorySessionStore(...)Gives this agent a session store that keeps sessions in the API's memory. MapAGUIServer finds it by itself and uses it for every request
withIsolation: falseTurns off isolation, explained next

Isolation keeps one user's conversations away from everyone else's. With isolation on, the store files each session under the caller's identity as well as the threadId, so two users who pick the same threadId never see each other's conversation. For that, the API must know who the caller is — and it has no sign-in yet. With isolation on, every request fails with Agent isolation key is required but was not provided by the configured AgentIsolationKeyProvider (Fixed: Agent Isolation Key Is Required but Was Not Provided). On your own machine there is exactly one user — you — and Microsoft's own sample turns isolation off for development, and only there.

The complete file:

src/Dewiride.ServiceDesk.Api/Program.cs
using Dewiride.ServiceDesk.Agent;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Health checks answer one question: "is this API up and running?"
builder.Services.AddHealthChecks();

// The Dewiride Service Desk agent, and AG-UI: the protocol that streams its answers.
var serviceDeskAgent = builder.AddServiceDeskAgent();
builder.Services.AddAGUIServer();

// On your machine only: remember each conversation by its AG-UI threadId, in memory.
// Part 6 ties every conversation to a signed-in user; Part 12 stores them durably.
if (builder.Environment.IsDevelopment())
{
serviceDeskAgent.WithInMemorySessionStore(withIsolation: false);
}

var app = builder.Build();

app.MapHealthChecks("/health");
app.MapAGUIServer(serviceDeskAgent, "/api/agents/service-desk");

app.Run();

One using line and one if block, and the API remembers:

macOS-style code window titled Program.cs showing the Dewiride.ServiceDesk.Api Program.cs after Part 4: the agent and AG-UI registrations, then an if block that calls WithInMemorySessionStore with withIsolation false only when the environment is Development

Development only — on purpose

Outside Development, this API keeps no sessions yet: every request is a new conversation, exactly as in Part 3. That is deliberate. The in-memory store loses everything when the API stops, cannot be shared by two copies of the API, and — with isolation off — lets anyone who knows a threadId continue that conversation. Part 6 adds Microsoft Entra ID sign-in and turns isolation on; Part 12 replaces the in-memory store with durable storage.

Step 3: Build the Solution​

Terminal
dotnet build
Output
Restore complete (0.4s)
Dewiride.ServiceDesk.Agent net10.0 succeeded (0.1s) → src/Dewiride.ServiceDesk.Agent/bin/Debug/net10.0/Dewiride.ServiceDesk.Agent.dll
Dewiride.ServiceDesk.Api net10.0 succeeded (0.2s) → src/Dewiride.ServiceDesk.Api/bin/Debug/net10.0/Dewiride.ServiceDesk.Api.dll

Build succeeded in 0.8s

Still not a single warning. If your output looks different — Build succeeded., then 0 Warning(s) and 0 Error(s) on separate lines — your terminal is showing the older build output, and the build is just as good (Fixed: dotnet build Shows the Old Output Instead of Terminal Logger).

Step 4: Run the API​

Start the API exactly as in Step 1:

Terminal
dotnet run --project src/Dewiride.ServiceDesk.Api
Output
Using launch settings from src/Dewiride.ServiceDesk.Api/Properties/launchSettings.json...
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5180
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /Users/you/source/DewirideServiceDesk/src/Dewiride.ServiceDesk.Api

The same log as in Part 3 — the store is created quietly. One line matters more than before: Hosting environment: Development. That is what makes IsDevelopment() return true, so the session store is on. Leave the API running.

Step 5: Continue the Conversation​

Send the same two requests as in Step 1: first the Wi-Fi request, then the run-2 request. Nothing in them has changed — only the API has.

The first answer is the same kind of clarifying question:

The answer to run-1, put together
Which operating system is your laptop using: Windows or macOS?

And the response to run-2, shortened:

Response to run-2 (shortened)
HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Thu, 29 Oct 2026 16:15:58 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Pragma: no-cache
Transfer-Encoding: chunked

data: {"type":"RUN_STARTED","threadId":"thread-2","runId":"run-2","parentRunId":"run-1"}

data: {"type":"REASONING_ENCRYPTED_VALUE","subtype":"message","entityId":"msg_7uwPDaOflXROKYMciD4wAmXR","encryptedValue":"gAAAAABq…","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_START","messageId":"msg_07c08057…","role":"assistant","name":"service-desk","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_07c08057…","delta":"Try","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_07c08057…","delta":" these","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_07c08057…","delta":" steps","rawEvent":{…}}

… 50 more TEXT_MESSAGE_CONTENT events …

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_07c08057…","delta":" devices can connect.","rawEvent":{…}}

data: {"type":"TEXT_MESSAGE_END","messageId":"msg_07c08057…"}

data: {"type":"RUN_FINISHED","threadId":"thread-2","runId":"run-2","outcome":{"type":"success"},"usage":[{"model":"gpt-5.6-luna-2026-07-09","inputTokens":327,"outputTokens":255,"totalTokens":582,"reasoningTokens":50,"cachedInputTokens":0}]}

Put together:

The answer to run-2, put together
Try these steps on your Windows 11 laptop:

1. Make sure Wi-Fi is enabled:
- Select the network, sound, or battery icon on the taskbar.
- Confirm Wi-Fi is on and Airplane mode is off.

2. Select the office Wi-Fi network and choose Connect. Enter your normal work sign-in details if prompted. Do not share your password.

3. If it still fails, forget and reconnect:
- Settings > Network & internet > Wi-Fi > Manage known networks.
- Select the office network > Forget.
- Reconnect to it from the Wi-Fi list.

4. Restart the laptop and try again. If possible, check whether nearby colleagues can connect.

5. If you connect but have no internet, temporarily disconnect from any VPN and try opening a website.

6. If the problem continues, raise a ticket with the IT Service Desk. Include the Wi-Fi network name, the exact error message, and whether other devices can connect.

The very same request as in Step 1 — and this time the agent knew what "It's a Windows 11 laptop" answered. It gave Windows 11 steps for the office Wi-Fi, warned you not to share your password, and ended with a ticket and what to put in it, just as its instructions ask.

Compare "inputTokens":327 with the 246 of Step 1. The 81 extra tokens are the first turn, which Foundry put back into the conversation from its stored answers — the list you saw in Where the history lives.

Step 6: Add a Third Turn​

One more turn shows the pattern. Add this request below the run-2 request:

Dewiride.ServiceDesk.Api.http (add below the run-2 request)
# Keep the conversation going: run-3 continues run-2.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-2",
"runId": "run-3",
"parentRunId": "run-2",
"messages": [
{
"id": "message-3",
"role": "user",
"content": "I tried all of that, and it still won't connect."
}
]
}

###

The threadId stays thread-2. Two values move one step along: runId becomes run-3, and parentRunId becomes run-2, the turn this one follows. The message gets a new id too.

Send it. Its RUN_STARTED event names the chain, and RUN_FINISHED shows the conversation growing:

Response to run-3 (first and last event)
data: {"type":"RUN_STARTED","threadId":"thread-2","runId":"run-3","parentRunId":"run-2"}

data: {"type":"RUN_FINISHED","threadId":"thread-2","runId":"run-3","outcome":{"type":"success"},"usage":[{"model":"gpt-5.6-luna-2026-07-09","inputTokens":600,"outputTokens":108,"totalTokens":708,"reasoningTokens":0,"cachedInputTokens":0}]}

Put together:

The answer to run-3, put together
Since the basic fixes didn’t work, the issue may require checking your laptop’s Wi-Fi driver, account access, or the office network.

Please raise a ticket with the IT Service Desk and include:

1. Your Windows 11 laptop name or asset number, if known.
2. The office Wi-Fi network name.
3. The exact error message.
4. Whether other colleagues can connect.
5. Whether your laptop can connect to another Wi-Fi network or phone hotspot.

Do not include your password or MFA codes.

"All of that" only makes sense if the agent remembers the six steps it gave you — and it does. It knows the basic fixes are done, names what could still be wrong — the Wi-Fi driver, your account or the office network — and tells you to raise a ticket, with the details the service desk will need.

The model writes new words every time, so your answers will not match mine word for word — but they should follow the conversation the same way. The complete .http file:

src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.http
@baseUrl = http://localhost:5180

# Is the API up and running?
GET {{baseUrl}}/health

###

# Say hello to the service desk agent. The answer streams back as AG-UI events.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-1",
"runId": "run-1",
"messages": [
{
"id": "message-1",
"role": "user",
"content": "Hi! What can you help me with?"
}
]
}

###

# Ask for help with a real IT problem.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-2",
"runId": "run-1",
"messages": [
{
"id": "message-1",
"role": "user",
"content": "My laptop won't connect to the office Wi-Fi. What should I do?"
}
]
}

###

# Answer the agent's question in the same conversation: the same threadId, a new runId,
# and parentRunId set to the run it continues. Send only the new message.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-2",
"runId": "run-2",
"parentRunId": "run-1",
"messages": [
{
"id": "message-2",
"role": "user",
"content": "It's a Windows 11 laptop."
}
]
}

###

# Keep the conversation going: run-3 continues run-2.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-2",
"runId": "run-3",
"parentRunId": "run-2",
"messages": [
{
"id": "message-3",
"role": "user",
"content": "I tried all of that, and it still won't connect."
}
]
}

###

# Ask something that is not about IT. The agent should politely decline.
POST {{baseUrl}}/api/agents/service-desk
Content-Type: application/json
Accept: text/event-stream

{
"threadId": "thread-3",
"runId": "run-1",
"messages": [
{
"id": "message-1",
"role": "user",
"content": "Can you write me a short poem about pizza?"
}
]
}

###

Sending a request again adds another turn to its conversation. Want to start the Wi-Fi conversation over? Restart the API — the next step shows why that works.

Step 7: See What the In-Memory Store Forgets​

Stop the API (Ctrl+C) and start it again, as in Step 4. Then send only the run-2 request — "It's a Windows 11 laptop." — without the Wi-Fi request before it. Put together, the answer is:

The answer to run-2 after a restart, put together
What would you like help with on your Windows 11 laptop?

The conversation is gone, and RUN_FINISHED says so: "inputTokens":246 again, no history at all. The in-memory store lived inside the API process, so stopping the API emptied it. The new API found no session for thread-2 and started a fresh one. Foundry still holds the old answers for 30 days, but nothing points to them any more.

On your machine, that is fine — even handy, because a restart gives you a clean slate. For real users it is not, and that is exactly why Part 12 replaces this store with durable storage. Stop the API when you are done.

Your Solution Now Looks Like This​

DewirideServiceDesk/
├── .gitignore
├── DewirideServiceDesk.slnx
├── Directory.Build.props
├── Directory.Packages.props
├── global.json
└── src/
├── Dewiride.ServiceDesk.Agent/
│ ├── Dewiride.ServiceDesk.Agent.csproj
│ ├── FoundryOptions.cs
│ ├── ServiceDeskAgentExtensions.cs
│ └── ServiceDeskInstructions.cs
└── Dewiride.ServiceDesk.Api/
├── Properties/
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── Dewiride.ServiceDesk.Api.csproj
├── Dewiride.ServiceDesk.Api.http two follow-up requests (changed)
└── Program.cs the session store (changed)

No new files and no new packages: WithInMemorySessionStore was already in the hosting package from Part 3.

Common Mistakes​

MistakeWhat happensFix
Putting the whole conversation in messagesThe agent receives the earlier turns twice — Foundry already has them — and every turn costs moreSend only the new message (Fixed: AG-UI Agent Receives the Conversation Twice)
Calling WithInMemorySessionStore() without withIsolation: falseEvery request fails with 500 Internal Server ErrorAdd withIsolation: false, as in Step 2 (Fixed: Agent Isolation Key Is Required)
Moving the store outside the IsDevelopment() blockA deployed API would let anyone who guesses a threadId read and continue that conversationKeep it inside the block. Production waits for Parts 6 and 12
Changing the threadId for a follow-upThe follow-up starts a new conversation and the agent forgetsKeep the threadId of the conversation you continue
Sending a follow-up after restarting the APIThe agent has forgotten the earlier turnsSend the conversation's first request again, then the follow-ups

Troubleshooting​

SymptomCauseFix
Response: 500 Internal Server Error with Agent isolation key is required but was not provided by the configured AgentIsolationKeyProviderThe store was registered with isolation on, and the API has no sign-inUse WithInMemorySessionStore(withIsolation: false), as in Step 2
The agent forgets on every request, even right after the first oneThe API is not running in Development, so the store is never registeredCheck the log for Hosting environment: Development, and start the API with its launch profile, as in Step 4
You ran dotnet run -e Production to try another environment, and the log still says Hosting environment: Development-e sets an environment variable, so it needs the variable's nameUse -e ASPNETCORE_ENVIRONMENT=Production (Fixed: dotnet run -e Staging Still Starts the App in Development)
The agent forgot the conversation after you restarted the APIThe in-memory store empties when the API stopsExpected — send the first request of the conversation again
Answers repeat themselves and inputTokens jumps more than expectedThe client resent earlier messagesSend only the new message

Frequently Asked Questions​

Is the threadId a password?​

No — and it must never be treated as one. With isolation off, anyone who knows a threadId can continue that conversation, which is acceptable only because the API listens on your own machine. Microsoft's AG-UI guidance calls the thread ID a continuation identifier, not proof of identity. Part 6 adds sign-in and turns isolation on, so each conversation belongs to the user who started it.

How long does Foundry keep the conversation?​

By default, the Responses API keeps stored answers for 30 days. The sessions that point to them live in the API's memory in this part, so a restart forgets them long before that. Part 12 comes back to this when conversations are stored durably.

Do I have to send parentRunId?​

Send it. Microsoft's AG-UI guide asks for the same threadId plus the previous runId as parentRunId on every follow-up. In this version of the framework, the session is chosen by threadId alone; parentRunId is passed through to RUN_STARTED and to tracing, where it records which turn each run continued.

Why doesn't the client simply send the whole conversation every time?​

Without a session store, it could — that is how a stateless AG-UI server works. But then the client controls the history, and Microsoft's AG-UI security guidance warns that an untrusted client can slip fake assistant or system messages into it. In this series the server keeps the history, and the trusted server in Part 5 sends only the newest user message.

Does the agent remember me in a new conversation?​

No. A session belongs to one thread, so a new threadId starts with a blank page. Remembering facts about an employee across conversations — their laptop, their location — comes in Part 13.

What happens in Production right now?​

No session store is registered outside Development, so every request is a new conversation, as in Part 3. Nothing is deployed until Part 19, and by then Parts 6 and 12 have added sign-in and durable storage.

Conclusion​

The agent can finally hold a conversation:

  • a session store — in memory, in Development only — that keeps each conversation's session between requests,
  • follow-up requests that keep the threadId, take a new runId, name their parentRunId and carry only the new message,
  • the knowledge of where the history lives: in Foundry's stored answers, with the session pointing at the latest one,
  • and the limits of the in-memory store — lost on restart, isolation off — that later parts remove.

Next: Part 5: A Plug-and-Play Next.js Chat UI (coming soon) — a real chat page that streams the agent's answers word by word, with a trusted server route that sends only the newest message.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.