feat(api-server): support X-Hermes-Session-Id header for session continuity

Allow callers to pass X-Hermes-Session-Id in request headers to continue
an existing conversation. When provided, history is loaded from SessionDB
instead of the request body, and the session_id is echoed in the response
header. Without the header, existing behavior is preserved (new uuid per
request).

This enables web UI clients to maintain thread continuity without modifying
any session state themselves — the same mechanism the gateway uses for IM
platforms (Telegram, Discord, etc.).
This commit is contained in:
txchen
2026-03-31 12:56:10 -07:00
committed by Teknium
parent 3ff9e0101d
commit bf19623a53
2 changed files with 128 additions and 4 deletions

View File

@@ -496,7 +496,22 @@ class APIServerAdapter(BasePlatformAdapter):
status=400,
)
session_id = str(uuid.uuid4())
# Allow caller to continue an existing session by passing X-Hermes-Session-Id.
# When provided, history is loaded from state.db instead of from the request body.
provided_session_id = request.headers.get("X-Hermes-Session-Id", "").strip()
if provided_session_id:
session_id = provided_session_id
try:
from hermes_state import SessionDB
_db = SessionDB()
history = _db.get_messages_as_conversation(session_id)
except Exception as e:
logger.warning("Failed to load session history for %s: %s", session_id, e)
history = []
else:
session_id = str(uuid.uuid4())
# history already set from request body above
completion_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
model_name = body.get("model", "hermes-agent")
created = int(time.time())
@@ -540,7 +555,7 @@ class APIServerAdapter(BasePlatformAdapter):
return await self._write_sse_chat_completion(
request, completion_id, model_name, created, _stream_q,
agent_task, agent_ref,
agent_task, agent_ref, session_id=session_id,
)
# Non-streaming: run the agent (with optional Idempotency-Key)
@@ -599,11 +614,11 @@ class APIServerAdapter(BasePlatformAdapter):
},
}
return web.json_response(response_data)
return web.json_response(response_data, headers={"X-Hermes-Session-Id": session_id})
async def _write_sse_chat_completion(
self, request: "web.Request", completion_id: str, model: str,
created: int, stream_q, agent_task, agent_ref=None,
created: int, stream_q, agent_task, agent_ref=None, session_id: str = None,
) -> "web.StreamResponse":
"""Write real streaming SSE from agent's stream_delta_callback queue.
@@ -620,6 +635,8 @@ class APIServerAdapter(BasePlatformAdapter):
cors = self._cors_headers_for_origin(origin) if origin else None
if cors:
sse_headers.update(cors)
if session_id:
sse_headers["X-Hermes-Session-Id"] = session_id
response = web.StreamResponse(status=200, headers=sse_headers)
await response.prepare(request)