Repository navigation
Description
Summary
Severity: High
Area: Persistence / Gemini
Affected files: agentkit/memory/redis_store.py:72-79, agentkit/core/agent.py:157-161, agentkit/tools/models.py:70-73
Suggested branch: fix/gemini-thought-signature-persistence
Suggested PR title: fix(memory): persist Gemini thought signatures losslessly
This is finding AUDIT-04 of 18 from a consolidated audit of agentkit/. It is deliberately self-contained so it can be fixed on its own branch with its own PR.
Related findings: Related: AUDIT-05 (same adapter), AUDIT-10 (silent persistence failures).
AUDIT-04 — High — Gemini thought_signature cannot be persisted; session history silently dropped
Files
agentkit/memory/redis_store.py:72-79agentkit/core/agent.py:157-161agentkit/tools/models.py:70-73(field definition)
Current code — agentkit/memory/redis_store.py:72-79
async def save_messages(self, session_id: str, messages: list[Message]) -> None:
"""Persist conversation messages in Redis with TTL and sliding window."""
if self.max_messages is not None:
messages = self.truncate_messages(messages, self.max_messages)
key = self._key(session_id)
serialized = json.dumps([m.model_dump(mode="json") for m in messages])
await self._client.set(key, serialized, ex=self.ttl_s)Current code — agentkit/core/agent.py:157-161
async def _finalize_and_return(result: RunResult) -> RunResult:
if session_id and self.memory is not None and hasattr(self.memory, "save_messages"):
_repair_trailing_tool_calls(messages)
with contextlib.suppress(Exception):
await self.memory.save_messages(session_id, messages)Why this is a bug
ToolCall.thought_signature is bytes | None (agentkit/tools/models.py:70-73). Pydantic's JSON mode defaults to ser_json_bytes="utf8" — it does not base64-encode. Gemini's signatures are raw binary, so model_dump(mode="json") raises UnicodeDecodeError for any non-UTF-8 signature, and save_messages aborts before writing anything. The agent's contextlib.suppress(Exception) then hides it: the entire conversation for that session is never persisted, and the next turn starts with no history.
Reproduced
thought_signature = b"\xff\xfe\x00\x81" -> model_dump(mode="json") raises UnicodeDecodeError
thought_signature = b"c2VjcmV0" -> dumped as the plain string "c2VjcmV0" (not base64)
Pass cases
-
thought_signatureround-trips byte-for-byte for arbitrary binary payloads (e.g.b"\xff\xfe\x00\x81",b"\x00", empty bytes) throughRedisMemoryStore.save_messages→get_messages. - The persisted representation is explicitly encoded (e.g. base64) and explicitly decoded on read — the encoding must not depend on pydantic defaults.
- A non-UTF-8 signature no longer aborts the save: the whole message list is persisted.
- A failure to persist the session is logged at ERROR with the session id and does not silently present the run as fully successful.
- A regression test asserts
Agent.run(session_id=…)with a Gemini-style non-UTF-8 signature results in a stored history that can be read back and re-sent to the provider. -
docs/DECISIONS.md:87(ADR-006's "transparently base64-encoded" claim) is corrected, anddocs/PROGRESS.md:354/docs/PLAN.md:155are aligned (see AUDIT-16).
Definition of done (per AGENTS.md)
- New/updated unit and integration tests covering the pass cases above, all passing (
pytest). -
ruff check .,ruff format --check .andmypy agentkitclean. - Docstrings updated where behaviour changed;
docs/PROGRESS.mdupdated (status, PR link, commit count, notes). - A new ADR in
docs/DECISIONS.mdif an architectural decision was made. - Branch pushed and PR opened (never commit directly to
main).
Baseline at the time of writing: pytest is green on main (all tests pass; tests/real/* skipped unless AGENTKIT_REAL_INFRA=1).
Activity
getdownandcode commentedon Oct 20, 2026
Audit index (18 separate issues, fix one by one)
Each issue is self-contained: current code, reproduction, pass cases, suggested branch and PR title. Reference the relevant one with
Fixes #NNin your PR.