-
Notifications
You must be signed in to change notification settings - Fork 813
Python: add ADR for threads in python #2420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
eavanvalkenburg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - 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. | ||
eavanvalkenburg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### 2. Separate classes for `ServiceThread`/`RemoteThread` and `LocalAgentThread`/`LocalThread`, each with their own behaviors and methods. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
eavanvalkenburg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - 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. | ||
eavanvalkenburg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - Bad, because it would more fundamentally diverge from dotnet. | ||
eavanvalkenburg marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@eavanvalkenburg Isn't it the same in Python?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
eavanvalkenburg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.