← Back to honi.dev

Graph Memory

Graph memory adds a fourth tier to Honi's memory stack — a persistent, queryable knowledge graph backed by edgraph, an edge-native property graph built on Cloudflare Durable Objects. Use it to store entities and relationships that your agent discovers, and retrieve them via traversal rather than keyword or vector search.

Why graph memory?

The other three tiers answer different questions:

TierAnswers
Working (DO)"What happened in this conversation?"
Episodic (D1)"What was said across all conversations?"
Semantic (Vectorize)"What's conceptually similar to this?"
Graph (edgraph)"How do these entities relate to each other?"

Graph memory is the right choice when your agent needs to reason about structure — org charts, customer relationships, investment portfolios, knowledge maps — rather than just recall past text.

edgraph

edgraph is a property graph database built on Cloudflare Durable Objects. One DO instance per graph. Multi-hop BFS/DFS traversal runs inside the DO using SQLite — zero per-hop network cost. It stores an adjacency index alongside the edges table so traversal cost is proportional to the local neighbourhood, not the total graph size.

Deploy your own edgraph instance:

Shell
1# Clone and deploy edgraph
2git clone https://github.com/stukennedy/edgraph
3cd edgraph && bun install
4wrangler secret put EDGRAPH_API_KEY
5wrangler deploy

Setup

Add edgraph as a CF service binding in your wrangler.toml (preferred — zero latency, CF-internal network) or use an HTTP URL:

TOML
1# Option A: service binding (recommended — same CF account)
2[[services]]
3binding = "EDGRAPH"
4service = "edgraph"
5
6# Option B: external HTTP
7[vars]
8EDGRAPH_URL = "https://edgraph.myapp.workers.dev"

Set the API key as a Worker secret:

Shell
1wrangler secret put EDGRAPH_API_KEY

Configure

Enable graph memory in createAgent():

TypeScript
1export const agent = createAgent({
2 name: 'crm-agent',
3 model: 'claude-sonnet-4-20250514',
4 memory: {
5 enabled: true,
6 graph: {
7 enabled: true,
8 graphId: 'crm-knowledge-base',
9 binding: 'EDGRAPH', // CF service binding
10 apiKeyEnvVar: 'EDGRAPH_API_KEY',
11 contextDepth: 1, // hop depth for context expansion
12 },
13 },
14})

All options for GraphConfig:

OptionTypeDefaultDescription
enabledbooleanEnable graph memory
graphIdstringGraph identifier. Maps to one edgraph DO instance.
bindingstringCF service binding name. Preferred over urlEnvVar.
urlEnvVarstringEnv var name whose value is the edgraph HTTP URL.
apiKeyEnvVarstringEnv var name for edgraph API key (required for writes).
contextDepthnumber1Hop depth for graph context expansion during retrieval.
maxContextEntitiesnumber5Max entities to expand per retrieval. Guards against large context blocks.

Writing to the graph from tools

Tool handlers receive an optional second argument — ctx — which includes the live GraphMemory instance. Use it to write entities as your tool discovers them:

TypeScript
1import { tool, z } from 'honidev'
2
3const lookupCustomer = tool({
4 name: 'lookup_customer',
5 description: 'Look up a customer by ID',
6 input: z.object({ id: z.string() }),
7 handler: async (input, ctx) => {
8 const customer = await db.getCustomer(input.id)
9
10 // Write to graph as we discover entities
11 if (ctx?.graph && customer) {
12 await ctx.graph.upsertNode(customer.id, 'Customer', {
13 name: customer.name, plan: customer.plan
14 })
15 if (customer.accountManagerId) {
16 await ctx.graph.upsertEdge(
17 customer.id, customer.accountManagerId, 'managed_by'
18 )
19 }
20 }
21
22 return customer
23 },
24})

ctx.graph is the live GraphMemory instance bound to the current agent. Entities written here are immediately available for future context retrieval — and they persist durably across sessions.

Hybrid retrieval (Semantic + Graph)

When both semantic and graph memory are enabled, Honi performs a two-stage retrieval on every request:

  1. Semantic search — embeds the user message and finds the top-K similar past episodes in Vectorize.
  2. Entity extraction — collects any entityId values from the semantic result metadata.
  3. Graph expansion — calls toContext(entityIds, depth) on those IDs, expanding each into its local neighbourhood.
  4. Context injection — both the semantic results and the graph context are prepended to the system prompt before the LLM sees the message.

The result: instead of "here are some similar past conversations", your agent gets the full structured picture of the entities involved — including relationships they've never been directly told about.

Using GraphMemory standalone

GraphMemory can be used independently of createAgent() — as a shared knowledge base across multiple services:

TypeScript
1import { GraphMemory } from 'honidev'
2
3const graph = new GraphMemory({
4 graphId: 'crm',
5 url: 'https://edgraph.myapp.workers.dev',
6 apiKey: process.env.EDGRAPH_API_KEY
7})
8
9// Write entities
10await graph.upsertNode('alice', 'Person', { role: 'CTO' })
11await graph.upsertNode('acme', 'Company', { industry: 'SaaS' })
12await graph.upsertEdge('alice', 'acme', 'works_at')
13
14// Traverse and generate LLM context
15const context = await graph.toContext(['alice'], 2)
16// → "[Knowledge graph context:]
17// (Person:alice) {role="CTO"}
18// → [works_at] → (Company:acme)"
19
20// Traversal
21const neighbours = await graph.getNeighbours('alice', 'out')
22const path = await graph.shortestPath('alice', 'bob')

GraphMemory API

MethodDescription
upsertNode(id, label, props)Create or update a node
upsertEdge(fromId, toId, type, props?)Create or update a directed edge
getNode(id)Fetch a single node by ID
listNodes(opts)List nodes, optionally filtered by label
getNeighbours(id, direction?, types?)Direct neighbours (in/out/both, filter by edge type)
traverse(from, opts)BFS or DFS traversal with depth, edge type, and node label filters
shortestPath(from, to)Shortest path between two nodes
subgraph(root, depth, direction)Extract a local subgraph around a root node
toContext(entityIds, depth?)Render entity subgraphs as an LLM-injectable text block
deleteNode(id)Delete a node
deleteEdge(id)Delete an edge
stats()Node and edge counts

Performance characteristics

OperationComplexity
Get node / edgeO(1) — primary key lookup
Get neighboursO(degree) — adjacency index
k-hop traversalO(k × avg_degree) — no full-table scan
Shortest path (BFS)O(V + E) over reachable subgraph
Subgraph extractionO(nodes + edges in subgraph)

All traversal runs inside the Durable Object's SQLite — a single HTTP call regardless of hop count.