Skip to content

Commit b75da16

Browse files
authored
Stateless user context support (#178)
* stateful/stateless clarification mode * Update core-modules.mdc * Update test_api_endpoints.py
1 parent ed76d2f commit b75da16

File tree

14 files changed

+354
-167
lines changed

14 files changed

+354
-167
lines changed

.cursor/rules/core-modules.mdc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ alwaysApply: true
216216
- `ChatCompletionRequest`: OpenAI-compatible request model
217217
- `MessagesList`: Root model for message lists with base64 truncation
218218
- `AgentStateResponse`: Agent state response model
219-
- `ClarificationRequest`: Clarification request model
220219
- `HealthResponse`: Health check response
221220

222221
### FastAPI Application (`sgr_agent_core/server/app.py`)

.webui_secret_key

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
UZhigVonEnhHdYTr

docs/en/sgr-api/SGR-Agent's-Workflow.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ sequenceDiagram
3232
Note over Agent: State: WAITING_FOR_CLARIFICATION
3333
Agent->>Tools: Execute clarification tool
3434
Tools->>API: Return clarifying questions
35-
API-->>Client: Stream clarification questions
36-
37-
Client->>API: POST /v1/chat/completions<br/>{"model": "agent_id", "messages": [...]}
38-
API->>Agent: provide_clarification()
35+
API-->>Client: Stream clarification questions with<br/>agent_id embedded in content
36+
37+
alt Stateless mode (full-context client)
38+
Client->>API: POST /v1/chat/completions<br/>{"model": "sgr_agent", "messages": [<br/> ..., "agent {id} started", ...]}<br/>Agent ID detected inside messages
39+
API->>Agent: provide_clarification(replace=True)<br/>Conversation fully replaced
40+
else Stateful mode (delta client)
41+
Client->>API: POST /v1/chat/completions<br/>{"model": "agent_id", "messages": [new replies]}
42+
API->>Agent: provide_clarification(replace=False)<br/>Messages appended to conversation
43+
end
3944
Note over Agent: State: RESEARCHING
40-
Agent->>Agent: Add clarification to context
4145
4246
else Tool: GeneratePlan
4347
Agent->>Tools: Execute plan generation

docs/en/sgr-api/SGR-Description-API.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,18 @@ Create a chat completion for research tasks. This is the main endpoint for inter
117117
- `max_tokens` (integer, optional, default: 1500): Maximum number of tokens for generation
118118
- `temperature` (float, optional, default: 0): Generation temperature (0.0-1.0). Lower values make output more deterministic
119119

120-
**Special Behavior - Clarification Requests:**
120+
**Special Behavior - Resuming an Agent in Clarification State:**
121121

122-
If `model` contains an agent ID (format: `{agent_name}_{uuid}`) and the agent is in `waiting_for_clarification` state, this endpoint will automatically route to the clarification handler instead of creating a new agent.
122+
This endpoint supports two ways to resume an agent that is waiting for clarification:
123+
124+
| Mode | Trigger | Conversation handling |
125+
|---|---|---|
126+
| **Stateless (full-context)** | Agent ID found anywhere inside `messages` text | Agent's conversation is **replaced entirely** with the incoming `messages` |
127+
| **Stateful (delta)** | Agent ID passed as the `model` field value | Incoming `messages` are **appended** to the existing conversation |
128+
129+
Use the **stateless mode** when integrating with a standard OpenAI-compatible chat UI that re-sends the full message history on every request. The agent detects its own ID in the message content, overwrites its conversation snapshot, and resumes execution.
130+
131+
Use the **stateful mode** when your client tracks context itself and only sends new messages as a delta. Pass the agent ID (format: `{agent_name}_{uuid}`) as the `model` field value.
123132

124133
**Response:**
125134

@@ -321,7 +330,10 @@ curl http://localhost:8010/agents/sgr_agent_12345-67890-abcdef/state
321330

322331
## POST `/agents/{agent_id}/provide_clarification`
323332

324-
Provide clarification to an agent that is waiting for input. Resumes agent execution after receiving clarification messages.
333+
Provide clarification to an agent that is waiting for input. Resumes agent execution after receiving clarification messages. This endpoint operates in **stateful (delta) mode**: the provided messages are *appended* to the agent's existing conversation history.
334+
335+
!!! tip "Alternative via `/v1/chat/completions`"
336+
If you are using a standard OpenAI-compatible client that re-sends the full message history on each turn, prefer the **stateless mode** of `POST /v1/chat/completions`: embed the agent ID anywhere in the message text (the format `agent {agent_id} started` is already included by the agent itself at startup) and send the full context as `messages`. The server will detect the ID, replace the agent's conversation with the incoming snapshot, and resume execution.
325337

326338
**Path Parameters:**
327339

@@ -342,7 +354,7 @@ Provide clarification to an agent that is waiting for input. Resumes agent execu
342354

343355
**Request Parameters:**
344356

345-
- `messages` (array, required): Clarification messages in OpenAI format (ChatCompletionMessageParam). Can contain multiple messages for complex clarifications.
357+
- `messages` (array, required): New clarification messages in OpenAI format (ChatCompletionMessageParam). These are appended to the existing conversation — send only the new user replies, not the full history.
346358

347359
**Request:**
348360

@@ -365,7 +377,7 @@ curl -X POST "http://localhost:8010/agents/sgr_agent_12345-67890-abcdef/provide_
365377

366378
**Streaming Response:**
367379

368-
Returns streaming response (SSE format) with continued research after clarification. The agent resumes execution from the point where it requested clarification.
380+
Returns a streaming SSE response with continued research after clarification. The agent resumes execution from the point where it requested clarification.
369381

370382
**Error Responses:**
371383

@@ -375,15 +387,19 @@ Returns streaming response (SSE format) with continued research after clarificat
375387
"detail": "Agent not found"
376388
}
377389
```
390+
- `400 Bad Request`: Agent is not in `waiting_for_clarification` state
391+
```json
392+
{
393+
"detail": "Agent is not waiting for clarification"
394+
}
395+
```
378396
- `500 Internal Server Error`: Error during clarification processing
379397
```json
380398
{
381399
"detail": "Error message"
382400
}
383401
```
384402

385-
**Note:** This endpoint can also be accessed via POST `/v1/chat/completions` by using the agent ID as the `model` parameter when the agent is in `waiting_for_clarification` state.
386-
387403
## DELETE `/agents/{agent_id}`
388404

389405
Cancel a running agent's execution and remove it from storage. If the agent is currently running, it will be cancelled first before removal.

docs/ru/sgr-api/SGR-Agent's-Workflow.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ sequenceDiagram
3232
Note over Agent: Состояние: WAITING_FOR_CLARIFICATION
3333
Agent->>Tools: Выполнить инструмент уточнения
3434
Tools->>API: Вернуть уточняющие вопросы
35-
API-->>Client: Поток уточняющих вопросов
36-
37-
Client->>API: POST /v1/chat/completions<br/>{"model": "agent_id", "messages": [...]}
38-
API->>Agent: provide_clarification()
35+
API-->>Client: Поток уточняющих вопросов<br/>(содержит agent_id в тексте)
36+
37+
alt Режим stateless (клиент шлёт полный контекст)
38+
Client->>API: POST /v1/chat/completions<br/>{"model": "sgr_agent", "messages": [<br/> ..., "agent {id} started", ...]}<br/>ID агента найден внутри messages
39+
API->>Agent: provide_clarification(replace=True)<br/>Разговор полностью заменяется
40+
else Режим stateful (клиент шлёт дельту)
41+
Client->>API: POST /v1/chat/completions<br/>{"model": "agent_id", "messages": [новые ответы]}
42+
API->>Agent: provide_clarification(replace=False)<br/>Сообщения дописываются к разговору
43+
end
3944
Note over Agent: Состояние: RESEARCHING
40-
Agent->>Agent: Добавить уточнение в контекст
4145
4246
else Инструмент: GeneratePlan
4347
Agent->>Tools: Выполнить генерацию плана

docs/ru/sgr-api/SGR-Description-API.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,18 @@ curl http://localhost:8010/v1/models
117117
- `max_tokens` (integer, опциональный, по умолчанию: 1500): Максимальное количество токенов для генерации
118118
- `temperature` (float, опциональный, по умолчанию: 0): Температура генерации (0.0-1.0). Меньшие значения делают вывод более детерминированным
119119

120-
**Особое поведение - Запросы на уточнение:**
120+
**Особое поведение — Возобновление агента в состоянии ожидания уточнения:**
121121

122-
Если `model` содержит ID агента (формат: `{agent_name}_{uuid}`) и агент находится в состоянии `waiting_for_clarification`, этот endpoint автоматически перенаправит запрос на обработчик уточнений вместо создания нового агента.
122+
Endpoint поддерживает два способа возобновить агент, находящийся в состоянии `waiting_for_clarification`:
123+
124+
| Режим | Триггер | Обработка разговора |
125+
|---|---|---|
126+
| **Stateless (полный контекст)** | ID агента обнаружен где-либо в тексте `messages` | Разговор агента **полностью заменяется** входящими `messages` |
127+
| **Stateful (дельта)** | ID агента передан в поле `model` | Входящие `messages` **дописываются** к существующему разговору |
128+
129+
Используйте **режим stateless**, когда интегрируетесь через стандартный OpenAI-совместимый клиент, который каждый раз пересылает полную историю сообщений. Агент автоматически обнаружит свой ID в тексте (сообщение вида `agent {agent_id} started` добавляется агентом в самом начале работы), заменит снимок разговора и возобновит выполнение.
130+
131+
Используйте **режим stateful**, когда клиент сам управляет контекстом и отправляет только новые сообщения-дополнения. Передайте ID агента (формат: `{agent_name}_{uuid}`) в поле `model`.
123132

124133
**Ответ:**
125134

@@ -321,7 +330,10 @@ curl http://localhost:8010/agents/sgr_agent_12345-67890-abcdef/state
321330

322331
## POST `/agents/{agent_id}/provide_clarification`
323332

324-
Предоставить уточнение агенту, который ожидает ввода. Возобновляет выполнение агента после получения сообщений уточнения.
333+
Предоставить уточнение агенту, который ожидает ввода. Возобновляет выполнение агента после получения сообщений уточнения. Endpoint работает в **stateful (дельта) режиме**: переданные сообщения *дописываются* к существующей истории разговора агента.
334+
335+
!!! tip "Альтернатива через `/v1/chat/completions`"
336+
Если вы используете стандартный OpenAI-совместимый клиент, который пересылает полную историю сообщений при каждом запросе, предпочтите **режим stateless** endpoint `POST /v1/chat/completions`: вставьте ID агента в любое место текста сообщений (агент сам добавляет `agent {agent_id} started` в начале работы) и передайте полный контекст в `messages`. Сервер обнаружит ID, заменит разговор агента новым снимком и возобновит выполнение.
325337

326338
**Параметры пути:**
327339

@@ -342,7 +354,7 @@ curl http://localhost:8010/agents/sgr_agent_12345-67890-abcdef/state
342354

343355
**Параметры запроса:**
344356

345-
- `messages` (array, обязательный): Сообщения уточнения в формате OpenAI (ChatCompletionMessageParam). Может содержать несколько сообщений для сложных уточнений.
357+
- `messages` (array, обязательный): Новые сообщения уточнения в формате OpenAI (ChatCompletionMessageParam). Дописываются к существующему разговору — передавайте только новые реплики пользователя, а не полную историю.
346358

347359
**Запрос:**
348360

@@ -365,7 +377,7 @@ curl -X POST "http://localhost:8010/agents/sgr_agent_12345-67890-abcdef/provide_
365377

366378
**Потоковый ответ:**
367379

368-
Возвращает потоковый ответ (формат SSE) с продолжением исследования после уточнения. Агент возобновляет выполнение с точки, где он запросил уточнение.
380+
Возвращает потоковый SSE-ответ с продолжением исследования после уточнения. Агент возобновляет выполнение с точки, где он запросил уточнение.
369381

370382
**Ошибки:**
371383

@@ -375,15 +387,19 @@ curl -X POST "http://localhost:8010/agents/sgr_agent_12345-67890-abcdef/provide_
375387
"detail": "Agent not found"
376388
}
377389
```
390+
- `400 Bad Request`: Агент не находится в состоянии `waiting_for_clarification`
391+
```json
392+
{
393+
"detail": "Agent is not waiting for clarification"
394+
}
395+
```
378396
- `500 Internal Server Error`: Ошибка при обработке уточнения
379397
```json
380398
{
381399
"detail": "Сообщение об ошибке"
382400
}
383401
```
384402

385-
**Примечание:** Этот endpoint также доступен через POST `/v1/chat/completions`, используя ID агента в качестве параметра `model`, когда агент находится в состоянии `waiting_for_clarification`.
386-
387403
## DELETE `/agents/{agent_id}`
388404

389405
Отменить выполнение запущенного агента и удалить его из хранилища. Если агент в данный момент выполняется, он будет сначала отменен, а затем удален.

sgr_agent_core/base_agent.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,22 @@ def get_tool_config(self, tool_class: Type[BaseTool]) -> BaseModel | dict[str, A
8080
base = getattr(self.config, base_attr, None) if base_attr and self.config else None
8181
return config_from_kwargs(config_model, base, raw)
8282

83-
async def provide_clarification(self, messages: list[ChatCompletionMessageParam]):
84-
"""Receive clarification from an external source (e.g. user input) in
85-
OpenAI messages format."""
83+
async def provide_clarification(
84+
self,
85+
messages: list[ChatCompletionMessageParam],
86+
replace_conversation: bool = False,
87+
) -> None:
88+
"""Receive clarification from an external source in OpenAI messages
89+
format.
90+
91+
Args:
92+
messages: Clarification messages in OpenAI ChatCompletionMessageParam format.
93+
replace_conversation: When True, clear the conversation
94+
before applying messages (continuing stateful conversation / stateless mode).
95+
Use this for stateless clients that re-send the full history on every turn.
96+
"""
97+
if replace_conversation:
98+
self.conversation = []
8699
self.conversation.extend(messages)
87100
self.conversation.append(
88101
{"role": "user", "content": PromptLoader.get_clarification_template(messages, self.config.prompts)}
@@ -262,8 +275,10 @@ async def _execute(self):
262275
This method contains the main agent execution logic. It is
263276
called by execute() which wraps it in an asyncio task.
264277
"""
265-
print("start messages: ", self.task_messages)
266278
self.logger.info(f"🚀 User provided {len(self.task_messages)} messages.")
279+
init_message = f"Agent {self.id} started\n"
280+
self.conversation.append({"role": "system", "content": init_message})
281+
self.streaming_generator.add_content_delta(init_message, "0-start")
267282
try:
268283
while self._context.state not in AgentStatesEnum.FINISH_STATES.value:
269284
self._context.iteration += 1

sgr_agent_core/server/endpoints.py

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
AgentListResponse,
1313
AgentStateResponse,
1414
ChatCompletionRequest,
15-
ClarificationRequest,
1615
HealthResponse,
16+
MessagesRequest,
1717
)
18+
from sgr_agent_core.utils import is_agent_id
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -150,15 +151,17 @@ async def get_available_models():
150151

151152

152153
@router.post("/agents/{agent_id}/provide_clarification")
153-
async def provide_clarification(agent_id: str, request: ClarificationRequest):
154-
try:
155-
agent = agents_storage.get(agent_id)
156-
if not agent:
157-
raise HTTPException(status_code=404, detail="Agent not found")
158-
159-
logger.info(f"Providing clarification to agent {agent.id}: {len(request.messages)} messages")
154+
async def provide_clarification(
155+
request: MessagesRequest,
156+
agent_id: str,
157+
) -> StreamingResponse:
158+
messages = list(request.messages.root)
159+
agent = agents_storage.get(agent_id)
160+
if not agent:
161+
raise HTTPException(status_code=404, detail="Agent not found")
160162

161-
await agent.provide_clarification(request.messages)
163+
try:
164+
await agent.provide_clarification(messages, replace_conversation=request.agent_id_from_messages is not None)
162165
return StreamingResponse(
163166
agent.streaming_generator.stream(),
164167
media_type="text/event-stream",
@@ -168,35 +171,24 @@ async def provide_clarification(agent_id: str, request: ClarificationRequest):
168171
"X-Agent-ID": str(agent.id),
169172
},
170173
)
171-
172174
except Exception as e:
173175
logger.error(f"Error completion: {e}")
174176
raise HTTPException(status_code=500, detail=str(e))
175177

176178

177-
def _is_agent_id(model_str: str) -> bool:
178-
"""Check if the model string is an agent ID (contains underscore and UUID-
179-
like format)."""
180-
return "_" in model_str and len(model_str) > 20
181-
182-
183179
@router.post("/v1/chat/completions")
184180
async def create_chat_completion(request: ChatCompletionRequest):
185181
if not request.stream:
186182
raise HTTPException(status_code=501, detail="Only streaming responses are supported. Set 'stream=true'")
187183

188-
# Check if this is a clarification request for an existing agent
184+
agent_id = request.agent_id_from_messages or (request.model if is_agent_id(request.model) else None)
189185
if (
190-
request.model
191-
and isinstance(request.model, str)
192-
and _is_agent_id(request.model)
193-
and request.model in agents_storage
194-
and agents_storage[request.model]._context.state == AgentStatesEnum.WAITING_FOR_CLARIFICATION
186+
agent_id is not None
187+
and agent_id in agents_storage
188+
and agents_storage[agent_id]._context.state == AgentStatesEnum.WAITING_FOR_CLARIFICATION
195189
):
196-
return await provide_clarification(
197-
agent_id=request.model,
198-
request=ClarificationRequest(messages=request.messages.root),
199-
)
190+
response = await provide_clarification(request, agent_id=agent_id)
191+
return response
200192

201193
try:
202194
agent_def = next(filter(lambda ad: ad.name == request.model, AgentFactory.get_definitions_list()), None)
@@ -210,7 +202,7 @@ async def create_chat_completion(request: ChatCompletionRequest):
210202
logger.info(f"Created agent '{request.model}' with {len(request.messages)} messages")
211203

212204
agents_storage[agent.id] = agent
213-
asyncio.create_task(agent.execute()) # Starts execution, task stored in agent._execute_task
205+
asyncio.create_task(agent.execute())
214206
return StreamingResponse(
215207
agent.streaming_generator.stream(),
216208
media_type="text/event-stream",

0 commit comments

Comments
 (0)