Skip to content
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/decisions/00XX-python-agent-threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
status: Proposed
contact: eavanvalkenburg
date: 2025-11-24
deciders: markwallace-microsoft, dmytrostruk, taochenosu, alliscode, moonbox3
consulted: sergeymenshykh, rbarreto, dmytrostruk, westey-m
---

# Agent Threads in Python

## Context and Problem Statement

Currently in Python we use `threads` in the following way: we have a single thread class, called `AgentThread`, which is responsible for holding either a `ChatMessageStore` or a `service_thread_id`, but not both.
- Generally you create a thread by calling `agent.get_new_thread()` which can take some arguments as well.
- It can then be used, if you use it by passing it to a `agent.run` or `agent.run_stream` call, with `store=True`, it will then attempt (if the chat client supports that) to create a thread in the service, we will then store the response_id as the `service_thread_id` in the `AgentThread` instance.
- If you use it with `store=False`, it will try to call the `ChatMessageStoreFactory` on the agent to get a new `ChatMessageStore` instance, which will then be stored in the `AgentThread` instance.
- However before all of that a thread is `uninitialized`, meaning it has neither a `ChatMessageStore` nor a `service_thread_id`.
- It also has a method called `on_new_messages` which does nothing when called to a thread that has a `service_thread_id`, but when called to a thread that has a `ChatMessageStore`, it will call the `add_messages` method of the `ChatMessageStore` instance.
This all means that at various moments a thread can be different things and have different behaviors, which can be confusing for users. It is also unclear which chat clients can support which type of thread, and what the implications are of using one or the other, in combination with the `store` parameter.

## Decision Drivers

- Ease of use: make it easy for users to understand how to use threads in the Python SDK -> Zen of Python: "Explicit is better than implicit".
- Clarity: make it clear what a thread is and what it can do.

## Considered Options

1. Current approach with a single `AgentThread` class that can hold either a `ChatMessageStore` or a `service_thread_id`.
1. Separate classes for remote thread and local thread, each with their own behaviors and methods.

### 1. Current approach with a single `AgentThread` class that can hold either a `ChatMessageStore` or a `service_thread_id`.
- Good, because it is a single class that can be used for both types of threads.
- Good, because it is easy to create a new thread by calling `agent.get_new_thread()`.
- Good, because it is consistent with dotnet.
- Bad, because it can be confusing for users to understand the different states of a thread.
- Bad, because it is unclear which chat clients can support which type of thread.
- Bad, because dotnet also has subclasses for each type of agent, so already somewhat diverging from dotnet.

### 2. Separate classes for `ServiceThread`/`RemoteThread` and `LocalAgentThread`/`LocalThread`, each with their own behaviors and methods.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the explicit-ness of the separate classes. I think doing it this way will be better for developers, too, as they'd know which type they're working with and what's available in each object.

This approach would mean:
- Creating two subclasses of AgentThread, one for service threads and one for local threads, both with `context providers` as attributes, but with different other attributes and methods.
- We would then add a flag on `ChatClients`, to indicate if they support `remote/service` threads, and we assume that we always support `local` threads.
- And finally, all Agents would get two methods, `get_service_thread(thread_id: str | None = None, ...)`/`get_remote_thread(thread_id: str | None = None, ...)` and `get_local_thread(chat_message_store: ...)`, both of which might raise an error if the chat client does not support that type of thread.
- the `run` methods would take both types of threads, but would raise an error if the thread type is not supported by the chat client.
- One open question is how to handle when there is a mismatch between the thread type and the `store` parameter, for example passing a `LocalAgentThread` with `store=True`, or a `ServiceAgentThread` with `store=False`. Options are:
- Raise an error
- The `store` and `conversation_id` parameters have precedence, so that if you pass in a `local` thread and `store=True` and `conversation_id!=None`, the messages from the thread are passed in, but the thread is not updated and overwritten with a remote thread after the call.
- The `Thread` has precedence over chat options. In other words, because the `thread` is defined at the agent level, while the `store` and `conversation_id` parameters are defined at the chat client level, the `thread` has precedence, so that if you pass a `RemoteThread`, it will set `store=True` regardless of what is passed in otherwise, and `conversation_id` will be set to the id in the `thread`, while if you pass a `LocalThread`, it will set `store=False` and `conversation_id=None` regardless of what is passed in otherwise.
- I believe the last option makes the most sense.
- Naming is another open question, options are:
- for the remote threads:
- `ServiceThread`
- `RemoteThread`
- `ServiceSideThread`
- for the local threads:
- `LocalAgentThread`
- `LocalThread`
- `ClientSideThread`

`RemoteThread` and `LocalThread` seem the clearest and most concise options and the most pythonic.

So that gives the following:
- Good, because it is explicit about the type of thread being used.
- Good, because it is clear which chat clients support which type of thread.
- Good, because we can make all the logic that deals with threads much clearer, as each class has a single responsibility.
- Good, because it might also enable a abstracted method to get a list of chat messages from a thread through the chat client.
- Bad, because it would more fundamentally diverge from dotnet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if this API brings more clarity, it can be introduced in .NET as well. Otherwise, I'm not sure if we want to diverge from .NET this much.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When discussing with Weslie we also came to the conclusion that we might invert the order, and do this for python and follow with .net

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main problem in .net is that we don't know what kind of thread is supported by the underlying service until we get the first response from the chat client and changing this will be challenging.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main problem in .net is that we don't know what kind of thread is supported by the underlying service until we get the first response from the chat client and changing this will be challenging.

@eavanvalkenburg Isn't it the same in Python?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, but since currently all the clients are built by us and part of AF, it's a onetime breaking change to add the flag to the protocol and base_client and then we are good to go, and if someone implements their own, it's a simple addition. For dotnet, this is a bit more tricky


## Decision Outcome

TBD
Loading