Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions integrations/agentbricks/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This Software is the property of Databricks, Inc. ("Databricks") and no license,

This Software contains code from the following open source projects, licensed under the MIT license (https://opensource.org/licenses/MIT).

Package: langgraph
Project URL: https://github.com/langchain-ai/langgraph
Copyright: 2024 LangChain, Inc.

Package: PyYAML
Project URL: https://github.com/yaml/pyyaml
Copyright: 2017-2021 Ingy döt Net, 2006-2016 Kirill Simonov
Expand Down
86 changes: 83 additions & 3 deletions integrations/agentbricks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,9 @@ an entry came from, for provenance). An entry is uniquely identified by its `act
optional `session_id`.

Write an entry when the agent learns something durable, then recall it in a later, separate
conversation with a natural-language search — results are ranked by full-text (BM25) relevance, up to
100 entries, with no pagination or vector similarity:
conversation with a natural-language search — results are ranked by relevance, full-text (BM25)
combined with semantic matching as the managed memory docs describe, up to 100 entries, with no
pagination:

```python
from databricks.sdk import WorkspaceClient
Expand Down Expand Up @@ -650,7 +651,9 @@ In an agent generated by `agentbricks init`, each invocation picks its session a
partition:

- **Session store:** one conversation's state, keyed by the invocation's top-level `session_id`.
- **Memory store:** facts recalled across conversations, keyed by `input.actor`.
- **Memory store:** facts recalled across conversations, keyed by `input.actor`. The
[LangGraph memory store](#langgraph-memory-store-as-a-basestore) keeps its entries under
`<actor>:langgraph` instead, so `memory_tools` doesn't list them.

If a request leaves out `actor`, it defaults to the `session_id`, so long-term memory won't carry
across conversations. For memory that persists, pass a stable `actor`, such as the end user's ID, from
Expand Down Expand Up @@ -695,6 +698,83 @@ removes the corresponding `AGENT_MEMORY_STORE` or `AGENT_SESSION_STORE` entry fr
Unbinding does not delete the store or revoke an existing grant to the App's service principal.
If access must be revoked, coordinate that separately with the store owner or workspace admin.

### LangGraph memory store as a BaseStore

`memory_store()` returns a LangGraph `BaseStore` backed by the managed memory store, so a graph keeps facts across conversations without a Postgres connection, and LangGraph and LangMem code that takes `store=` uses it as is. Build one store per app at startup, with no `actor=`, pass it to the graph with `store=`, and name each request's user through `thread_config`:

```python
from databricks_agentkit.langgraph import checkpointer, memory_store, thread_config
from langchain.agents import create_agent

store = memory_store() # once, at startup: no actor= here

agent = create_agent(model, tools=tools, checkpointer=checkpointer(), store=store)

# Per request. user_id is the signed-in user, from your app's trusted identity.
config = thread_config(session_id, actor=user_id)
result = await agent.ainvoke({"messages": inputs}, config=config)
```

Inside a node or tool, `get_store()` from `langgraph.config` returns the same store, for example `await get_store().aput(("preferences",), "style", {"value": "terse"})`. Values are JSON objects (dicts).

**Where entries live.** Each item is one cross-session memory entry at `/langgraph/<namespace>/<key>`, holding the value in a small JSON envelope (`{"lg": 1, "v": <value>}`, plus `"x"`, the expiry in epoch milliseconds, for an item with a TTL). Entries are kept under the storage actor `<user-id>:langgraph`, not the plain actor, so `memory_tools` and sessions, which use the plain actor, don't show them. Memory pipelines find the actors they process through sessions, and no session uses that id, so automatic pipeline runs leave these entries alone. That's a convention rather than a service guarantee: `thread_config`, the checkpointer, `memory_tools` and the OpenAI session and memory helpers refuse actors ending with `:langgraph`, but sessions created another way aren't checked, and a pipeline run that names its actors explicitly can still reach the entries. For the strongest separation, give the store a memory store that no pipeline writes to. Deleting a user's data includes deleting `<user-id>:langgraph`. `DatabricksMemoryStore.storage_actor(user_id)` returns the id. To look at a user's entries:

```bash
agentbricks -o json memory entries list --store <store-id> --actor-id "<user-id>:langgraph" --path-prefix /langgraph/
```

**Who the actor is.** Take it from the identity your app receives from the trusted ingress (the bundled UI reads the `x-forwarded-email` or `x-forwarded-user` header), never from a request body field or a model-chosen value. The bundled `agent-langgraph` template still takes `input.actor` from the request body, so don't add `memory_store()` to it until its actor comes from the forwarded identity. A resumed run, such as one continuing after an approval, needs the same actor as the original run. `memory_store(actor=...)` is for calls outside a LangGraph run, such as scripts and tests; inside a run, a run for a different actor raises instead of mixing users. `thread_config(session_id)` without an actor uses the session id, so memories last for one conversation, and the store logs a warning.

**No store configured.** The store resolves `store=`, then `AGENT_MEMORY_STORE`, on first use. Deployed Apps get the variable from the `[memory_store]` binding in agent.toml. `agentbricks dev` doesn't set it, so export it in your shell to use a dev store locally. With neither, a local run uses an in-process stand-in partitioned by actor, which keeps nothing across restarts, and a deployed App raises.

**LangMem.** Use the namespace `("memories",)`, since the actor already partitions entries:

```python
from langmem import create_manage_memory_tool

manage = create_manage_memory_tool(namespace=("memories",), store=store)
await manage.ainvoke({"action": "create", "content": "Prefers email over phone."}, config=thread_config(session_id, actor=user_id))
```

LangMem's default namespace, `("memories", "{langgraph_user_id}")`, needs `langgraph_user_id` in the run config. Without it LangMem raises `ConfigurationError`, a `BaseException` that `except Exception` misses; in a background `ReflectionExecutor` it kills the worker thread, so later submissions never complete. If you keep that namespace, set `langgraph_user_id` to the same user and build the store with `memory_store(strict_user_key="langgraph_user_id")`, which refuses a run where the two differ. Keep LangMem's memory manager and its manage-memory tool on separate namespaces, because the manager can't read the tool's entries.

**Python 3.10.** LangGraph's `get_config()` and `get_store()` don't work in async code on 3.10. There, build the store per request with `memory_store(actor=user_id)`, compile the graph with it, and use that store object directly instead of `get_store()`.

Limits:

- **Search** ranks with the managed memory service, which the Agent Bricks docs describe as full-text (BM25) combined with semantic matching over each entry's content and description. The response doesn't say which matching ran (see the check below). Scores are the service's rank-based values: higher is better, but they aren't similarities and there's no cutoff, so a query can return entries that share no words with it. Client-side embeddings and `index` configs aren't supported.
- **Results.** A query returns at most 100 entries, with no pagination; `limit` defaults to 10, and an `offset` of 100 or more returns nothing. `filter` runs after ranking: when the service returns its full 100, a filtered page can come back short, and the store logs a warning. Expired entries count toward the 100 until they're swept.
- **Browsing.** `asearch(namespace)` with no query reads that namespace's entries, 100 per request, until the page is full; `alist_namespaces()` reads every entry the user has. Keep both out of hot paths.
- **Writes.** `aput` is last-writer-wins and not atomic, and so are batches. A call that raised may still have been applied; retrying it is safe. An existing key costs three API calls, a new one one. A put can raise `ABORTED` when another writer changes the same key at once; retry it. The SDK retries transient errors for up to five minutes before a call fails, and cancelling an async call doesn't stop a write already sent. Deletes are hard deletes, and deleting an entry that is already gone succeeds.
- **Values.** Values must be JSON objects: other types raise `TypeError`, and NaN, infinity and values JSON can't encode are refused. Tuples come back as lists and non-string keys as strings. Each put also stores the value's text, up to 500 characters, as the entry's description, which listings show and search matches. Requests are capped at about 900 KB and the actor plus path at about 2.4 KB. An entry changed with `agentbricks memory entries update --content` no longer holds the envelope, so the store skips it.

**TTL.** `aput(..., ttl=minutes)`, or `memory_store(ttl={"default_ttl": minutes})` for puts that don't pass `ttl=`, sets an expiry. It differs from LangGraph's Postgres store:

- A TTL counts from the last write, on the app's clock (keep hosts NTP-synced). Reads never refresh it: the store sets `refresh_on_read` to False, `refresh_on_read=True` raises, and per-call `refresh_ttl` arguments are ignored.
- Expired items aren't returned (`omit_expired` defaults to True), but they stay in the service until they're overwritten or swept, and the CLI still lists them under `<user-id>:langgraph`.
- There's no background sweeper: `sweep_interval_minutes` raises, because the memory API lists one actor at a time. Call `store.sweep_ttl(actor=user_id)` (or `asweep_ttl`) to delete one user's expired items, for example after a request or from a scheduled job. A put that lands between a sweep's check and its delete can be lost.
- A put without a TTL clears an earlier one. Zero, negative, non-finite and unknown TTL settings raise.

**Checking whether a workspace has semantic matching.** The response doesn't say, so test it with a throwaway store:

1. Write two entries under one test actor: "Prefers to be contacted by email rather than phone." and "Allergic to peanuts."
2. Search "electronic mail correspondence", which shares no word with either entry, retrying for up to two minutes while the service indexes the entries. If the email entry comes back first, semantic matching is on. If nothing comes back, matching is full-text only.
3. Delete the test store.

The gated live test `tests/suites/test_langgraph_store.py::test_search_mode` runs the same check.

**The actor partitions data; it is not access control. The store is the boundary.** Calls run with the app's credentials, so any principal with store access can read every actor's entries, and any workspace user can list store names. Use a separate store per tenant or trust domain. `agentbricks deploy` grants the app's service principal access to a store declared in agent.toml. A failed grant is reported and does not fail the deploy. For a store named only in code, grant access with the app's `service_principal_client_id`:

```python
from databricks_agentkit import workspace_client
from databricks_agentkit.client import AgentKitClient

client = AgentKitClient(workspace_client())
client.memory_stores.get("<store-id>").grant_permission("<app-client-id>")
```

**Coming from `DatabricksStore`.** `databricks_langchain.DatabricksStore` keeps memories in a Lakebase Postgres table you own and ranks with an embedding endpoint you configure. This store needs neither. [Migrate from DatabricksStore](docs/migrate-from-databricks-store.md) covers the code and data changes.

## Model and prompt upgrades (experimental)

`agentbricks experimental models` and `agentbricks experimental prompts` find the best model and
Expand Down
Loading