Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
06981f8
feat(agent-memory): add create_checkpointer() factory for LangGraph a…
santosh-nallur Jul 3, 2026
fb16425
test(agent-memory): add unit tests for create_checkpointer() factory
santosh-nallur Jul 3, 2026
563583f
docs(agent-memory): document LangGraph checkpointer factory in user g…
santosh-nallur Jul 3, 2026
03e957d
Remove unnecessary ticket reference from test docstring
santosh-nallur Jul 3, 2026
e9b83b0
Add logger for in-memory checkpointer usage
santosh-nallur Jul 6, 2026
36213a7
fix(agent-memory): upgrade InMemorySaver log from info to warning in …
santosh-nallur Jul 6, 2026
28af397
feat(agent-memory): add TimedInMemorySaver with TTL and update factory
santosh-nallur Jul 7, 2026
ed6d8c4
docs(agent-memory): document ttl_seconds and TimedInMemorySaver in us…
santosh-nallur Jul 8, 2026
7ec8c83
refactor(agent-memory): remove TimedInMemorySaver, retain ttl_seconds…
santosh-nallur Jul 9, 2026
e7fb5d6
docs(agent-memory): remove premature @agent_config TTL example
santosh-nallur Jul 9, 2026
6664b36
feat(agent-memory): restore TimedInMemorySaver with TTL support
santosh-nallur Jul 9, 2026
3306006
docs(agent-memory): update user guide for TimedInMemorySaver with TTL
santosh-nallur Jul 9, 2026
2df4371
Apply review comment
santosh-nallur Jul 10, 2026
19ba72c
feat(agent-memory): add langgraph optional dependency and update prer…
santosh-nallur Jul 10, 2026
0c2bbbc
fix(agent-memory): bump langgraph optional dep lower bound to >=1.0.0
santosh-nallur Jul 10, 2026
9a553cb
docs(agent-memory): add @agent_config example for TTL configuration
santosh-nallur Jul 10, 2026
08f01fa
docs(agent-memory): update thread_ttl_seconds comment to clarify dura…
santosh-nallur Jul 10, 2026
cc896e8
docs (agent-memory): update user-guide.md
santosh-nallur Jul 10, 2026
ed689ea
revert pre-commit dependency
santosh-nallur Jul 10, 2026
e6e125a
feat(agent-memory): update langgraph installation instruction
santosh-nallur Jul 10, 2026
1673f0f
feat(agent-memory): update langgraph installation instruction
santosh-nallur Jul 10, 2026
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
extensibility = ["a2a-sdk>=0.2.0"]
starlette = ["starlette>=0.40.0"]
langchain = ["langchain-core>=1.2.7"]
langgraph = ["langgraph>=1.0.0"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@cassiofariasmachado do we need to make anything to install it conditionally to langchain install?


[build-system]
requires = ["hatchling"]
Expand Down
14 changes: 14 additions & 0 deletions src/sap_cloud_sdk/agent_memory/factory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Factory subpackage for SAP Agent Memory framework adapters.
Each module in this package provides a factory function that creates the
appropriate framework-specific implementation for the current environment.
Available factories:
- :func:`sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint.create_checkpointer`
— returns a LangGraph ``BaseCheckpointSaver`` (short-term memory)
Usage:
from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer
"""
111 changes: 111 additions & 0 deletions src/sap_cloud_sdk/agent_memory/factory/_timed_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""TimedInMemorySaver — InMemorySaver with background TTL-based thread eviction.

Not intended for direct use. Use ``create_checkpointer(ttl_seconds=...)`` instead.
"""

import logging
import threading
import time
from typing import Any

from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import ChannelVersions, Checkpoint, CheckpointMetadata
from langgraph.checkpoint.memory import InMemorySaver

logger = logging.getLogger(__name__)

_SWEEP_INTERVAL_SECONDS = 60


class TimedInMemorySaver(InMemorySaver):
"""InMemorySaver with background TTL-based thread eviction.

Tracks last-active time per thread and evicts inactive threads via a
daemon background sweep. The sweep is fully decoupled from the read/write
path — ``put()`` only updates the activity timestamp.

This approximates server-side TTL semantics for in-process storage:
- ``put()`` records last-active timestamp (hot path stays clean)
- A daemon sweep thread evicts inactive threads every 60 seconds
- Eviction is best-effort — a thread may live up to
``ttl_seconds + 60`` seconds before deletion
- State does not survive process restarts (inherent to InMemorySaver)

Args:
ttl_seconds: Inactivity threshold in seconds. Threads inactive for
longer than this are evicted. Default: 3600 (1 hour).

Example::

from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import (
create_checkpointer,
)

checkpointer = create_checkpointer(ttl_seconds=3600)
"""

def __init__(self, *, ttl_seconds: int = 3600, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.ttl_seconds = ttl_seconds
self._last_active: dict[str, float] = {}
self._lock = threading.Lock()
self._sweeper = threading.Thread(
target=self._sweep_loop,
daemon=True,
name="TimedInMemorySaver-sweeper",
)
self._sweeper.start()
logger.debug(
"TimedInMemorySaver started with ttl_seconds=%d, sweep_interval=%ds",
ttl_seconds,
_SWEEP_INTERVAL_SECONDS,
)

def put(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Save checkpoint and refresh thread activity timestamp."""
thread_id: str = config["configurable"]["thread_id"]
with self._lock:
self._last_active[thread_id] = time.monotonic()
return super().put(config, checkpoint, metadata, new_versions)

async def aput(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Async version of put — refreshes thread activity timestamp."""
thread_id: str = config["configurable"]["thread_id"]
with self._lock:
self._last_active[thread_id] = time.monotonic()
return await super().aput(config, checkpoint, metadata, new_versions)

def _sweep_loop(self) -> None:
while True:
time.sleep(_SWEEP_INTERVAL_SECONDS)
self._evict_expired()

def _evict_expired(self) -> None:
now = time.monotonic()
with self._lock:
expired = [
tid
for tid, ts in self._last_active.items()
if now - ts > self.ttl_seconds
]
for tid in expired:
self.delete_thread(tid)
with self._lock:
self._last_active.pop(tid, None)
logger.info(
"TimedInMemorySaver: evicted inactive thread '%s' (inactive for >%ds)",
tid,
self.ttl_seconds,
)
75 changes: 75 additions & 0 deletions src/sap_cloud_sdk/agent_memory/factory/langgraph_checkpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""LangGraph checkpointer factory for SAP Agent Memory.

Usage::

from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer

# No TTL — plain InMemorySaver
checkpointer = create_checkpointer()

# With TTL — TimedInMemorySaver evicts inactive threads automatically
checkpointer = create_checkpointer(ttl_seconds=3600)

app = workflow.compile(checkpointer=checkpointer)

# Or with LangChain create_agent:
from langchain.agents import create_agent
agent = create_agent(model="...", tools=[...], checkpointer=checkpointer)
"""

import logging
from typing import Optional

logger = logging.getLogger(__name__)


def create_checkpointer(*, ttl_seconds: Optional[int] = None):
"""Create a LangGraph checkpointer for the current environment.

Returns LangGraph's ``InMemorySaver`` (no TTL) or ``TimedInMemorySaver``
(with TTL-based thread eviction). State is held in-process and does not
survive restarts.

Args:
ttl_seconds: Evict threads inactive for this many seconds.
``None`` (default) disables eviction.

Returns:
BaseCheckpointSaver instance.

Raises:
ImportError: If langgraph is not installed.

Example — no TTL::

checkpointer = create_checkpointer()
app = workflow.compile(checkpointer=checkpointer)

Example — evict threads inactive for 1 hour::

checkpointer = create_checkpointer(ttl_seconds=3600)
app = workflow.compile(checkpointer=checkpointer)
"""
try:
from langgraph.checkpoint.memory import InMemorySaver
except ImportError:
raise ImportError(
"langgraph is required for create_checkpointer(). "
"Install it with: pip install sap-cloud-sdk[langgraph] or pip install langgraph"
)

if ttl_seconds is not None:
from sap_cloud_sdk.agent_memory.factory._timed_memory import TimedInMemorySaver

logger.warning(
"create_checkpointer(): using TimedInMemorySaver(ttl_seconds=%d) — "
"session state is in-process only and will be lost on process exit.",
ttl_seconds,
)
return TimedInMemorySaver(ttl_seconds=ttl_seconds)

logger.warning(
"create_checkpointer(): using InMemorySaver — "
"session state is in-process only and will be lost on process exit."
)
return InMemorySaver()
123 changes: 123 additions & 0 deletions src/sap_cloud_sdk/agent_memory/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ plain text, and the service makes it searchable by meaning.
- [`AgentMemoryNotFoundError` when fetching a resource](#agentmemorynotfounderror-when-fetching-a-resource)
- [`AgentMemoryHttpError` with status 401](#agentmemoryhttperror-with-status-401)
- [Configuration](#configuration)
- [Service Binding](#service-binding)
- [Mounted Secrets (Kubernetes)](#mounted-secrets-kubernetes)
- [Environment Variables](#environment-variables)
- [UAA JSON Schema](#uaa-json-schema)
- [LangGraph Checkpointer](#langgraph-checkpointer)
- [Prerequisites](#prerequisites)
- [Import](#import-1)
- [Usage with LangGraph StateGraph](#usage-with-langgraph-stategraph)
- [Usage with LangChain create\_agent](#usage-with-langchain-create_agent)
- [Thread TTL](#thread-ttl)

## Installation

Expand Down Expand Up @@ -806,3 +816,116 @@ The `uaa` key must contain a JSON string with the XSUAA credentials:
"url": "https://subdomain.authentication.region.hana.ondemand.com"
}
```

## LangGraph Checkpointer

> This section covers the LangGraph-specific
> factory for short-term memory (checkpointing). It is only relevant if your agent
> is built with LangGraph or LangChain's `create_agent()`. The core
> `AgentMemoryClient` and `create_client()` above are framework-agnostic and work
> independently of this section.

For LangGraph agents, the `agent_memory` module provides a `create_checkpointer()`
factory in the `factory` subpackage. It returns a `BaseCheckpointSaver` that
manages short-term session memory — conversation state, thread continuity, and
HITL support — natively within LangGraph.

> [!NOTE]
> The current implementation uses LangGraph's `InMemorySaver` or
> `TimedInMemorySaver` (when `ttl_seconds` is set). State is held in-process
> and does not survive restarts. Persistent checkpointing backed by the
> Agent Memory Service is not yet supported.

### Prerequisites

`langgraph` is an optional dependency. Install it via the SDK extra or directly:

```bash
# Via SDK optional extra (recommended)
pip install "sap-cloud-sdk[langgraph]"

# Or install langgraph directly
pip install langgraph
```

### Import

```python
from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer
```

### Usage with LangGraph StateGraph

```python
from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer

checkpointer = create_checkpointer()
app = workflow.compile(checkpointer=checkpointer)

result = app.invoke(
{"messages": [{"role": "user", "content": "hello"}]},
{"configurable": {"thread_id": "session-1"}},
)
```

### Usage with LangChain create_agent

```python
from langchain.agents import create_agent
from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer

agent = create_agent(
model="...",
tools=[...],
checkpointer=create_checkpointer(),
)
```

### Thread TTL

Pass `ttl_seconds` to evict threads that have been inactive for the given
period. This prevents unbounded memory growth in long-running processes.

```python
# Evict threads inactive for more than 1 hour
checkpointer = create_checkpointer(ttl_seconds=3600)
```

When `ttl_seconds` is set, the factory returns a `TimedInMemorySaver` that
tracks last-active time per thread and evicts inactive threads via a
background daemon sweep. Eviction is best-effort — a thread may live up to
`ttl_seconds + 60` seconds before deletion.

#### Exposing TTL as a configurable parameter with `@agent_config`

Use `@agent_config` from `sap_cloud_sdk.agent_decorators` to expose
`ttl_seconds` as an operator-adjustable configuration field. The key
`config.checkpointer.ttl_seconds` groups it with other checkpointer settings
so external tooling can surface it in the low-code UI alongside model
selection and temperature.

```python
from sap_cloud_sdk.agent_decorators import agent_config
from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer


@agent_config(
key="config.checkpointer.ttl_seconds",
label="Thread TTL (seconds)",
description="Evict inactive conversation threads after this period of "
"inactivity. Set to 0 to disable eviction.",
)
def thread_ttl_seconds() -> int:
return 3600 # 1 hour


class MyAgent:
def __init__(self):
ttl = thread_ttl_seconds()
self._checkpointer = create_checkpointer(ttl_seconds=ttl or None)
```

> [!NOTE]
> `TimedInMemorySaver` state does not survive process restarts — the TTL
> applies to in-process memory only. Persistent TTL enforcement will be
> available when the Agent Memory Service checkpointer ships.
Loading
Loading