You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Hermes Agent memory provider on Upstash Redis (DX-3182) - #80
Resolves the rest of DX-3182. Brings the Hermes Agent memory provider from #76 to main.
Why this PR
#76 was stacked on #75. #75 was squash-merged into main, and #76 was merged 91 seconds later into #75's branch, which by then was already merged. So the provider never reached main, which still has only the scaffold.
17ee9cc: one e2e fix. The control session now gets its own HERMES_HOME. Hermes's built-in session_search reads every session in state.db, whoever it belonged to, so in a shared home the control user could find sessions 1 and 2 there. That happened once in a local run, even though the provider's Redis searches returned nothing for that user.
If a user id with : is refused, initialize() has already created the stores, so the provider stays active as user default.
/undo keeps an undone turn when the user message and the reply both repeat exactly.
Before the release
The PyPI pending trusted publisher for upstash-hermes-memory must exist before the Version Packages PR is merged: owner upstash, repo agentkit, workflow release.yml, no environment.
* feat(hermes-memory): Upstash Redis memory provider for Hermes Agent (DX-3182)
Python ports of AgentMemory and ChatHistory that write the TS SDK's keys, JSON documents and
Redis Search indexes byte for byte, and UpstashMemoryProvider on top of them: per-turn recall,
per-session searchable transcripts, mirroring of the built-in memory tool, and four
upstash_* tools. Registered through the hermes_agent.memory_providers entry point.
Adds unit and live-Redis tests, and e2e/hermes_e2e.py, which drives a real Hermes session in a
temporary HERMES_HOME and checks what lands in Redis (and that the TS SDK recalls it).
* fix(hermes-memory): address the review of #76 (DX-3182)
- Search recovers when another client drops the shared index: an empty result is checked
against SEARCH.COUNT (-1 = missing), and the index is recreated and the query retried once.
- Metadata may not set text, userId or createdAt, so it cannot move a memory to another user.
- Memory ids may contain ':' like in the TS SDK; user and session ids still may not.
- The provider creates the index before its first write and waits for indexing after saving a
fact, so a fact saved in one turn is recalled in the next or in a session started right after.
- Tests create indexes before seeding and wait for indexing before asserting, instead of polling.
- e2e: Hermes's built-in stores are off; the sessions run back to back; session 2 must answer
with no tool calls (read from state.db); a control session as another user must not know.
* fix(hermes-memory): one-to-one key encoding, per-profile prefixes, /undo (DX-3182)
- User and session ids are percent-encoded (':' -> %3A, '%' -> %25) instead of mapping ':' to
'_', so 'alice:work' and 'alice_work' no longer share a partition. Ids with neither character
are unchanged and still match the TS SDK's keys.
- UPSTASH_HERMES_MEMORY_PREFIX / _CHAT_PREFIX are read through get_secret like the credentials,
so a multiplexed gateway uses each profile's own .env.
- on_session_switch(rewound=True) marks the session; the next sync_turn (or on_session_end) cuts
the saved transcript back to the latest user message Hermes still has, matching repeated
messages by occurrence, before appending. Undone turns no longer stay searchable.
* fix(hermes-memory): keep TS ids literal, match undo by turn after compaction (DX-3182)
- User/session ids are used as they are, like the TS SDK; a `:` is refused
(user id) or skips the transcript (session id) instead of being encoded,
since any encoding could equal another literal TS id.
- /undo finds the cut by the latest saved turn whose user text and reply
match the latest turn left in Hermes's history, so in-place compaction
no longer cuts retained turns.
---------
Co-authored-by: Upstash Box <box@upstash.com>
…ME (DX-3182)
Hermes's built-in session_search reads every session in state.db, whoever it belonged to, so in a
shared home the control user could find sessions 1 and 2 there and the check failed even though
the provider's Redis searches returned nothing for that user.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
An explicit zero TTL leaves transcripts stored indefinitely, and forgetting reports success before the search index reflects deletion, allowing removed memories to remain retrievable.
Brings the Hermes memory provider from #76 onto main, replacing the Python scaffold with Redis-backed memory and chat history shared with the TypeScript SDK.
Changes:
Adds Python memory primitives and a Hermes provider for automatic recall, transcript storage, and memory tools.
Adds contract, lifecycle, packaging, and end-to-end checks, with an isolated home for the control user.
Documents setup and queues the first 0.1.0 release through Changesets.
File
Description
README.md
Replaces scaffold status with the provider’s capabilities.
python/hermes-memory/tests/test_redis.py
Tests live Redis operations and shared data compatibility.
python/hermes-memory/tests/test_provider.py
Tests provider tools, lifecycle, isolation, and transcript rewind.
python/hermes-memory/tests/test_package.py
Checks public exports, registration, and wheel contents.
…182)
save_chat skipped EXPIRE for a TTL of 0 (a falsy check), so the transcript never expired. The TS
ChatHistory applies any TTL that is defined, 0 included (expire now); the port now does the same.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Python’s save logic preserves an existing title when passed an explicit empty string, preventing callers from clearing it and diverging from TypeScript behavior.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Treat an empty title as an explicit request to clear it
Passing title="" after a titled save preserves the old title here, whereas TypeScript ChatHistory.saveChat treats an explicitly empty title as clearing it (packages/sdk/src/chat-history.ts:225). A Python caller therefore cannot clear an existing title and writes a different document for the same operation. Fall back to the existing title only when title is None.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves the rest of DX-3182. Brings the Hermes Agent memory provider from #76 to
main.Why this PR
#76 was stacked on #75. #75 was squash-merged into
main, and #76 was merged 91 seconds later into #75's branch, which by then was already merged. So the provider never reachedmain, which still has only the scaffold.What's in it
main. Its tree is identical to what Hermes Agent memory provider on Upstash Redis (DX-3182) #76 merged; Support Python packages in the monorepo (DX-3182) #75's final tree equalledmain, so it applied without conflicts. The code and its review history are in Hermes Agent memory provider on Upstash Redis (DX-3182) #76.HERMES_HOME. Hermes's built-insession_searchreads every session instate.db, whoever it belonged to, so in a shared home the control user could find sessions 1 and 2 there. That happened once in a local run, even though the provider's Redis searches returned nothing for that user.upstash-hermes-memory: minorchangeset from Hermes Agent memory provider on Upstash Redis (DX-3182) #76, so merging this opens a Version Packages PR that bumps the package to0.1.0.Tested on this branch
pnpm py:lintandnode scripts/sync-version.mjs --checkpass.python/hermes-memory/e2e/hermes_e2e.pypasses in a real Hermes (v0.20.2,gpt-5-mini):Still open from #76's review (not in this PR)
:is refused,initialize()has already created the stores, so the provider stays active as userdefault./undokeeps an undone turn when the user message and the reply both repeat exactly.Before the release
The PyPI pending trusted publisher for
upstash-hermes-memorymust exist before the Version Packages PR is merged: ownerupstash, repoagentkit, workflowrelease.yml, no environment.