1717from contextlib import AbstractAsyncContextManager , AsyncExitStack , asynccontextmanager
1818from logging import Logger
1919from typing import Self , final , override
20+ from uuid import uuid4
2021
2122from pydantic import BaseModel
2223
2324from splunklib .ai .base_agent import BaseAgent
25+ from splunklib .ai .conversation_store import ConversationStore
2426from splunklib .ai .core .backend import AgentImpl
2527from splunklib .ai .core .backend_registry import get_backend
2628from splunklib .ai .messages import AgentResponse , BaseMessage , OutputT
@@ -107,6 +109,29 @@ class Agent(BaseAgent[OutputT]):
107109 logger:
108110 Optional logger instance used for tracing and debugging the agent's execution.
109111 Additionally logs from the local tools are forwarded to this logger.
112+
113+ conversation_store:
114+ Optional `ConversationStore` instance used to persist conversation history
115+ across multiple `invoke` calls. When provided, the agent automatically loads
116+ prior messages for the active thread before each invocation and saves the
117+ full updated history afterwards.
118+
119+ Use the built-in `InMemoryStore` for in-process persistence, or implement
120+ `ConversationStore` to back history with an external store.
121+
122+ Without a store, each `invoke` call is stateless and the agent has no memory
123+ of previous turns.
124+
125+ thread_id:
126+ Identifies the conversation thread used when reading from and writing to the
127+ `conversation_store`. Each unique `thread_id` maintains a separate history,
128+ so different users or sessions can share one store without interference.
129+
130+ If omitted, a random ID is generated automatically. The `thread_id` can
131+ also be overridden per-call by passing it directly to `invoke`.
132+
133+ Never invoke an Agent using the same thread_id more than once concurrently
134+ while using the same conversation_store.
110135 """
111136
112137 _impl : AgentImpl [OutputT ] | None
@@ -129,17 +154,22 @@ def __init__(
129154 name : str = "" , # Only used by Subagents
130155 description : str = "" , # Only used by Subagents
131156 logger : Logger | None = None ,
157+ conversation_store : ConversationStore | None = None ,
158+ thread_id : str | None = None ,
132159 ) -> None :
133160 super ().__init__ (
134161 model = model ,
135162 system_prompt = system_prompt ,
136163 name = name ,
137164 description = description ,
165+ tools = None ,
138166 agents = agents ,
139167 input_schema = input_schema ,
140168 output_schema = output_schema ,
141169 middleware = middleware ,
142170 logger = logger ,
171+ conversation_store = conversation_store ,
172+ thread_id = thread_id if thread_id is not None else str (uuid4 ()),
143173 )
144174
145175 self ._use_mcp_tools = use_mcp_tools
@@ -242,12 +272,19 @@ async def __aexit__(
242272 self ._agent_context_manager = None
243273 return result
244274
275+ # TODO: for now we have a thread_id as an optional param, should
276+ # we wrap it in a dataclass? Might help with future-proofing the API??
245277 @override
246- async def invoke (self , messages : list [BaseMessage ]) -> AgentResponse [OutputT ]:
278+ async def invoke (
279+ self , messages : list [BaseMessage ], thread_id : str | None = None
280+ ) -> AgentResponse [OutputT ]:
247281 if not self ._impl :
248282 raise AssertionError ("Agent must be used inside 'async with'" )
249283
250- return await self ._impl .invoke (messages )
284+ if thread_id is None :
285+ thread_id = self ._thread_id
286+
287+ return await self ._impl .invoke (messages , thread_id )
251288
252289
253290def _local_tools_path () -> tuple [str | None , str ]:
0 commit comments