Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
26c7932
docs: restructure memory documentation
abrookins Sep 25, 2025
22c1cde
docs: rename Memory Strategies to Memory Extraction Strategies and in…
abrookins Sep 25, 2025
ee979ff
Change optimize_query default from True to False across all interfaces
abrookins Sep 25, 2025
45e3e61
Skip flaky test_judge_comprehensive_grounding_evaluation
abrookins Sep 25, 2025
8f68ce4
Fix context percentage calculation returning null when model info pro…
abrookins Sep 25, 2025
f48bc33
Bump version to 0.12.2
abrookins Sep 26, 2025
a1bb191
Update memory documentation to clarify working memory persistence
abrookins Sep 26, 2025
af4bfa8
Add transparent working memory reconstruction from long-term storage
abrookins Sep 26, 2025
562d897
feat: Add recent_messages_limit parameter and fix critical extraction…
abrookins Sep 26, 2025
6d2a944
Add UpdateWorkingMemory schema for PUT requests to remove session_id …
abrookins Sep 26, 2025
f683dac
Fix flaky test that expects exact promotion count
abrookins Sep 26, 2025
163023c
Skip flaky test_multi_entity_conversation
abrookins Sep 26, 2025
db0db08
Fix datetime.UTC import for Python 3.10/3.11 compatibility
abrookins Sep 26, 2025
a081cfc
Bump client version to 0.12.2
abrookins Sep 26, 2025
5bb4915
Remove test logic from production code
abrookins Sep 26, 2025
67f1ee3
Fix count_memories method and test fallback behavior
abrookins Sep 29, 2025
2abefe1
Fix client test mock for synchronous json() method
abrookins Sep 29, 2025
215bbe8
Simplify count_memories to use proper vector search interface
abrookins Sep 29, 2025
2cca4af
Restructure memory documentation and add missing eager creation tool
abrookins Sep 29, 2025
f41aaca
Remove 'message' memory type from tool creation/editing schemas
abrookins Sep 29, 2025
ea5f617
Fix example agents to work with new working memory API behavior
abrookins Sep 29, 2025
cfbc478
Fix test to allow search_memory to include message type in enum
abrookins Sep 29, 2025
f18af75
Change Docker release to manual workflow dispatch
abrookins Sep 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 0 additions & 46 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,49 +65,3 @@ jobs:
uv run pytest --run-api-tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

docker:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract version from __init__.py
id: version
run: |
VERSION=$(grep '__version__ =' agent_memory_server/__init__.py | sed 's/__version__ = "\(.*\)"/\1/' || echo "latest")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
redislabs/agent-memory-server:latest
redislabs/agent-memory-server:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
88 changes: 88 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Release Docker Images

on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (leave empty to use version from __init__.py)'
required: false
type: string
push_latest:
description: 'Also tag as latest'
required: true
type: boolean
default: true

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Determine version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
else
VERSION=$(grep '__version__ =' agent_memory_server/__init__.py | sed 's/__version__ = "\(.*\)"/\1/' || echo "latest")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version to release: $VERSION"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security control: Static Code Analysis Semgrep Pro

Yaml.Github-Actions.Security.Run-Shell-Injection.Run-Shell-Injection

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

Severity: HIGH

Learn more about this issue


Why should you fix this issue?
This code introduces a vulnerability that could compromise the security of your production environment. In production, where reliability and security are paramount, even a small vulnerability can be exploited to cause significant damage, leading to unauthorized access or service disruption.


Jit Bot commands and options (e.g., ignore issue)

You can trigger Jit actions by commenting on this PR review:

  • #jit_ignore_fp Ignore and mark this specific single instance of finding as “False Positive”
  • #jit_ignore_accept Ignore and mark this specific single instance of finding as “Accept Risk”
  • #jit_ignore_type_in_file Ignore any finding of type "yaml.github-actions.security.run-shell-injection.run-shell-injection" in .github/workflows/release.yml; future occurrences will also be ignored.
  • #jit_undo_ignore Undo ignore command

- name: Build tags list
id: tags
run: |
TAGS="redislabs/agent-memory-server:${{ steps.version.outputs.version }}"
TAGS="$TAGS,ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}"

if [ "${{ inputs.push_latest }}" = "true" ]; then
TAGS="$TAGS,redislabs/agent-memory-server:latest"
TAGS="$TAGS,ghcr.io/${{ github.repository }}:latest"
fi

echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "Tags to push: $TAGS"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security control: Static Code Analysis Semgrep Pro

Yaml.Github-Actions.Security.Run-Shell-Injection.Run-Shell-Injection

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

Severity: HIGH

Learn more about this issue


Why should you fix this issue?
This code introduces a vulnerability that could compromise the security of your production environment. In production, where reliability and security are paramount, even a small vulnerability can be exploited to cause significant damage, leading to unauthorized access or service disruption.


Jit Bot commands and options (e.g., ignore issue)

You can trigger Jit actions by commenting on this PR review:

  • #jit_ignore_fp Ignore and mark this specific single instance of finding as “False Positive”
  • #jit_ignore_accept Ignore and mark this specific single instance of finding as “Accept Risk”
  • #jit_ignore_type_in_file Ignore any finding of type "yaml.github-actions.security.run-shell-injection.run-shell-injection" in .github/workflows/release.yml; future occurrences will also be ignored.
  • #jit_undo_ignore Undo ignore command

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.version.outputs.version }}
release_name: Release v${{ steps.version.outputs.version }}
body: |
Docker images published:
- `redislabs/agent-memory-server:${{ steps.version.outputs.version }}`
- `ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}`
${{ inputs.push_latest && format('- `redislabs/agent-memory-server:latest`{0}- `ghcr.io/{1}:latest`', '\n ', github.repository) || '' }}
draft: false
prerelease: false
1 change: 1 addition & 0 deletions agent-memory-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ working_memory = WorkingMemory(
messages=[
MemoryMessage(role="user", content="Hello!"),
MemoryMessage(role="assistant", content="Hi there! How can I help?")
# created_at timestamps are automatically set for proper chronological ordering
],
namespace="chat-app"
)
Expand Down
2 changes: 1 addition & 1 deletion agent-memory-client/agent_memory_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
memory management capabilities for AI agents and applications.
"""

__version__ = "0.12.1"
__version__ = "0.12.2"

from .client import MemoryAPIClient, MemoryClientConfig, create_memory_client
from .exceptions import (
Expand Down
146 changes: 129 additions & 17 deletions agent-memory-client/agent_memory_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from pydantic import BaseModel
from ulid import ULID

from .exceptions import MemoryClientError, MemoryServerError, MemoryValidationError
from .exceptions import (
MemoryClientError,
MemoryNotFoundError,
MemoryServerError,
MemoryValidationError,
)
from .filters import (
CreatedAt,
Entities,
Expand Down Expand Up @@ -364,8 +369,15 @@ async def get_or_create_working_memory(
return (True, created_memory)

return (False, existing_memory)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
except (httpx.HTTPStatusError, MemoryNotFoundError) as e:
# Handle both HTTPStatusError and MemoryNotFoundError for 404s
is_404 = False
if isinstance(e, httpx.HTTPStatusError):
is_404 = e.response.status_code == 404
elif isinstance(e, MemoryNotFoundError):
is_404 = True

if is_404:
# Session doesn't exist, create it
empty_memory = WorkingMemory(
session_id=session_id,
Expand Down Expand Up @@ -885,14 +897,6 @@ async def search_long_term_memory(
)
response.raise_for_status()
data = response.json()
# Some tests may stub json() as an async function; handle awaitable
try:
import inspect

if inspect.isawaitable(data):
data = await data
except Exception:
pass
return MemoryRecordResults(**data)
except httpx.HTTPStatusError as e:
self._handle_http_error(e.response)
Expand Down Expand Up @@ -1477,8 +1481,8 @@ def get_add_memory_tool_schema(cls) -> dict[str, Any]:
},
"memory_type": {
"type": "string",
"enum": ["episodic", "semantic", "message"],
"description": "Type of memory: 'episodic' (events/experiences), 'semantic' (facts/preferences), 'message' (conversation snippets)",
"enum": ["episodic", "semantic"],
"description": "Type of memory: 'episodic' (events/experiences), 'semantic' (facts/preferences)",
},
"topics": {
"type": "array",
Expand Down Expand Up @@ -1595,8 +1599,8 @@ def edit_long_term_memory_tool_schema(cls) -> dict[str, Any]:
},
"memory_type": {
"type": "string",
"enum": ["episodic", "semantic", "message"],
"description": "Updated memory type: 'episodic' (events/experiences), 'semantic' (facts/preferences), 'message' (conversation snippets)",
"enum": ["episodic", "semantic"],
"description": "Updated memory type: 'episodic' (events/experiences), 'semantic' (facts/preferences)",
},
"namespace": {
"type": "string",
Expand All @@ -1620,6 +1624,67 @@ def edit_long_term_memory_tool_schema(cls) -> dict[str, Any]:
},
}

@classmethod
def create_long_term_memory_tool_schema(cls) -> dict[str, Any]:
"""
Get OpenAI-compatible tool schema for creating long-term memories directly.

Returns:
Tool schema dictionary compatible with OpenAI tool calling format
"""
return {
"type": "function",
"function": {
"name": "create_long_term_memory",
"description": (
"Create long-term memories directly for immediate storage and retrieval. "
"Use this for important information that should be permanently stored without going through working memory. "
"This is the 'eager' approach - memories are created immediately in long-term storage. "
"Examples: User preferences, important facts, key events that need to be searchable right away. "
"For episodic memories, include event_date in ISO format."
),
"parameters": {
"type": "object",
"properties": {
"memories": {
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The memory content to store",
},
"memory_type": {
"type": "string",
"enum": ["episodic", "semantic"],
"description": "Type of memory: 'episodic' (events/experiences), 'semantic' (facts/preferences)",
},
"topics": {
"type": "array",
"items": {"type": "string"},
"description": "Optional topics for categorization",
},
"entities": {
"type": "array",
"items": {"type": "string"},
"description": "Optional entities mentioned in the memory",
},
"event_date": {
"type": "string",
"description": "Optional event date for episodic memories (ISO 8601 format: '2024-01-15T14:30:00Z')",
},
},
"required": ["text", "memory_type"],
},
"description": "List of memories to create",
},
},
"required": ["memories"],
},
},
}

@classmethod
def delete_long_term_memories_tool_schema(cls) -> dict[str, Any]:
"""
Expand Down Expand Up @@ -1674,6 +1739,7 @@ def get_all_memory_tool_schemas(cls) -> Sequence[dict[str, Any]]:
cls.get_add_memory_tool_schema(),
cls.get_update_memory_data_tool_schema(),
cls.get_long_term_memory_tool_schema(),
cls.create_long_term_memory_tool_schema(),
cls.edit_long_term_memory_tool_schema(),
cls.delete_long_term_memories_tool_schema(),
cls.get_current_datetime_tool_schema(),
Expand Down Expand Up @@ -1706,6 +1772,7 @@ def get_all_memory_tool_schemas_anthropic(cls) -> Sequence[dict[str, Any]]:
cls.get_add_memory_tool_schema_anthropic(),
cls.get_update_memory_data_tool_schema_anthropic(),
cls.get_long_term_memory_tool_schema_anthropic(),
cls.create_long_term_memory_tool_schema_anthropic(),
cls.edit_long_term_memory_tool_schema_anthropic(),
cls.delete_long_term_memories_tool_schema_anthropic(),
cls.get_current_datetime_tool_schema_anthropic(),
Expand Down Expand Up @@ -1764,6 +1831,12 @@ def get_long_term_memory_tool_schema_anthropic(cls) -> dict[str, Any]:
openai_schema = cls.get_long_term_memory_tool_schema()
return cls._convert_openai_to_anthropic_schema(openai_schema)

@classmethod
def create_long_term_memory_tool_schema_anthropic(cls) -> dict[str, Any]:
"""Get create long-term memory tool schema in Anthropic format."""
openai_schema = cls.create_long_term_memory_tool_schema()
return cls._convert_openai_to_anthropic_schema(openai_schema)

@classmethod
def edit_long_term_memory_tool_schema_anthropic(cls) -> dict[str, Any]:
"""Get edit long-term memory tool schema in Anthropic format."""
Expand Down Expand Up @@ -2143,6 +2216,11 @@ async def resolve_function_call(
elif function_name == "get_long_term_memory":
result = await self._resolve_get_long_term_memory(args)

elif function_name == "create_long_term_memory":
result = await self._resolve_create_long_term_memory(
args, effective_namespace, user_id
)

elif function_name == "edit_long_term_memory":
result = await self._resolve_edit_long_term_memory(args)

Expand Down Expand Up @@ -2287,6 +2365,40 @@ async def _resolve_get_long_term_memory(
result = await self.get_long_term_memory(memory_id=memory_id)
return {"memory": result}

async def _resolve_create_long_term_memory(
self, args: dict[str, Any], namespace: str | None, user_id: str | None = None
) -> dict[str, Any]:
"""Resolve create_long_term_memory function call."""
memories_data = args.get("memories")
if not memories_data:
raise ValueError(
"memories parameter is required for create_long_term_memory"
)

# Convert dict memories to ClientMemoryRecord objects
from .models import ClientMemoryRecord, MemoryTypeEnum

memories = []
for memory_data in memories_data:
# Apply defaults
if namespace and "namespace" not in memory_data:
memory_data["namespace"] = namespace
if user_id and "user_id" not in memory_data:
memory_data["user_id"] = user_id

# Convert memory_type string to enum if needed
if "memory_type" in memory_data:
memory_data["memory_type"] = MemoryTypeEnum(memory_data["memory_type"])

memory = ClientMemoryRecord(**memory_data)
memories.append(memory)

result = await self.create_long_term_memory(memories)
return {
"status": result.status,
"message": f"Created {len(memories)} memories successfully",
}

async def _resolve_edit_long_term_memory(
self, args: dict[str, Any]
) -> dict[str, Any]:
Expand Down Expand Up @@ -2757,7 +2869,7 @@ async def memory_prompt(
context_window_max: int | None = None,
long_term_search: dict[str, Any] | None = None,
user_id: str | None = None,
optimize_query: bool = True,
optimize_query: bool = False,
) -> dict[str, Any]:
"""
Hydrate a user query with memory context and return a prompt ready to send to an LLM.
Expand Down Expand Up @@ -2861,7 +2973,7 @@ async def hydrate_memory_prompt(
memory_type: dict[str, Any] | None = None,
limit: int = 10,
offset: int = 0,
optimize_query: bool = True,
optimize_query: bool = False,
) -> dict[str, Any]:
"""
Hydrate a user query with long-term memory context using filters.
Expand Down
Loading