AI Agents

Real-Time Context for AI Agents

An agent is only as good as the facts it is shown. This guide covers how to turn Kafka topics and database change streams into a live, consistent context object the agent reads with one query, how that compares to feature stores, vector databases and Flink, and a complete reference implementation you can run today.

Last reviewed 2026-11-08. Covers RisingWave 2.6 and later.

1s
Context Freshness
Every view commits a consistent snapshot each barrier, one second by default. Context is seconds behind the event, not hours.
1 SELECT
Per Agent Turn
Context objects are precomputed as materialized views. The agent reads one row instead of running six queries and a join.
vector(n)
Embeddings In-Stream
openai_embedding runs inside the view. HNSW indexes make retrieval a SQL ORDER BY.
SQL + MCP
Any Agent Framework
PostgreSQL wire protocol for every driver, plus an MCP server with 100+ tools for Claude, Cursor and friends.

The Problem

Why do AI agents need real-time context?

Most agent failures in production are not reasoning failures. They are context failures: the agent was shown facts that were wrong by the time it acted. A support agent promises a refund on an order that shipped two minutes ago. A procurement agent reorders stock that a cancelled order just returned. A recommendation agent pushes a product the customer bought in the current session. The model did exactly what the context told it to do.

The usual fix is to give the agent more tools and let it look everything up itself. That trades one problem for three: each turn becomes slow because the tool calls run in sequence, the prompt fills with raw rows the model has to aggregate, and the calls see different moments in time, so the context can contradict itself. Teams then add a cache, a stream job to fill the cache, a vector database, and a batch job to re-embed documents. Five systems now stand between an event and the agent that needs it.

The alternative is to precompute the context. Define each context object as a query, keep the result current as events arrive, and let the agent read it in one call. That is what incrementally maintained materialized views do, and it is the reason a streaming database is a natural context layer for agents.

DimensionContext assembled per turnContext maintained as views
Who computes the contextThe agent, with 4 to 8 tool calls per turnRisingWave, once, as events arrive
Latency per turnSum of all tool calls, often 1 to 3 secondsOne point lookup, milliseconds
Tokens spent on contextRaw rows from every tool callA compact precomputed object
ConsistencyEach tool call sees a different momentAll fields from one snapshot
Freshness of embeddingsLast batch embedding jobSeconds after the document arrives
Pieces to operateAgent code, stream job, cache, vector DB, embedding jobOne streaming database

Architecture

What does a streaming context layer look like?

The architecture has four layers, and only one of them is new. Events already flow through Kafka or come out of operational databases as change data capture. The agent loop already exists. The serving path is SQL over the PostgreSQL wire protocol or MCP, which agent frameworks already speak. The new layer is the set of materialized views in between: one per context object, each defined in SQL and maintained incrementally. In RisingWave those views, the embeddings inside them and the vector indexes over them live in one system, with state on object storage and a consistent snapshot across every view at each one-second barrier.

Five kinds of context cover most agents. Each maps to a well-understood view shape.

Context kindView shapeIn the reference implementation
Entity stateOne row per entity, joined from several sources, keyed by the entity ID.customer_360: tier, lifetime value, open tickets, last order, current risk flag.
Recent activityAggregates over a sliding or hopping window, keyed by entity.session_activity: pages viewed, cart changes and errors in the last 15 minutes.
Live statusLatest event per key, usually a CDC table or a deduplicated stream.order_status_live: current fulfilment state and carrier ETA for each open order.
Retrievable knowledgeAppend-only documents with an embedding column and a vector index.article_embeddings: help articles and policy pages, embedded as they are published.
Agent memoryA table the agent writes to, with views that summarise it per entity.agent_memory: promises made, escalations, preferences learned in earlier conversations.

The agent never touches the raw streams. It reads the views, and because every view is updated from the same barrier, the profile, the recent activity and the open orders it sees all describe the same instant. That consistency is what tool-call assembly cannot give you, and it is what stops an agent from reasoning about a refund that already happened.

Reference Implementation

A customer-support agent with live context, in SQL and Python

The scenario is an online shop. Orders and clickstream events arrive on Kafka, customers and support tickets live in PostgreSQL, and help articles are published to a topic. The agent answers customer questions and must know the current state of their orders, what they were just doing on the site, the latest help content, and what earlier conversations promised. Everything below runs against a single-node RisingWave; swap the connection details for RisingWave Cloud in production.

Step 1. Start RisingWave

Shell
# One-node RisingWave for development. SQL port 4566, dashboard 5691.
docker run -it --pull=always -p 4566:4566 -p 5691:5691 \
  risingwavelabs/risingwave:latest single_node

# Connect with any PostgreSQL client.
psql -h localhost -p 4566 -d dev -U root

Step 2. Connect the streams and tables

Kafka topics become append-only sources. The PostgreSQL tables become CDC tables through one shared CDC source, so updates to a customer's tier or a ticket's status flow through as changes rather than new rows. Watermarks on the event streams let the time windows below close correctly.

SQL (RisingWave)
-- Append-only event streams from Kafka.
CREATE SOURCE orders (
  order_id    BIGINT,
  customer_id BIGINT,
  amount      NUMERIC,
  status      VARCHAR,          -- placed, paid, shipped, delivered, refunded
  order_ts    TIMESTAMPTZ,
  WATERMARK FOR order_ts AS order_ts - INTERVAL '30 seconds'
) WITH (
  connector = 'kafka',
  topic = 'orders',
  properties.bootstrap.server = 'broker:9092'
) FORMAT PLAIN ENCODE JSON;

CREATE SOURCE clickstream (
  customer_id BIGINT,
  event_type  VARCHAR,          -- page_view, add_to_cart, checkout_error
  page        VARCHAR,
  event_ts    TIMESTAMPTZ,
  WATERMARK FOR event_ts AS event_ts - INTERVAL '10 seconds'
) WITH (
  connector = 'kafka',
  topic = 'clickstream',
  properties.bootstrap.server = 'broker:9092'
) FORMAT PLAIN ENCODE JSON;

-- Help articles are published to a topic; every edit is a new version.
CREATE SOURCE help_articles (
  article_id  BIGINT,
  version     INT,
  title       VARCHAR,
  body        VARCHAR,
  published_at TIMESTAMPTZ
) WITH (
  connector = 'kafka',
  topic = 'help_articles',
  properties.bootstrap.server = 'broker:9092'
) FORMAT PLAIN ENCODE JSON;

-- Operational tables via CDC. Updates and deletes flow through.
CREATE SOURCE shop_pg WITH (
  connector = 'postgres-cdc',
  hostname = 'pg', port = '5432',
  username = 'rw', password = '...',
  database.name = 'shop', schema.name = 'public'
);

CREATE TABLE customers (
  customer_id BIGINT PRIMARY KEY,
  name        VARCHAR,
  tier        VARCHAR,
  region      VARCHAR
) FROM shop_pg TABLE 'public.customers';

CREATE TABLE tickets (
  ticket_id   BIGINT PRIMARY KEY,
  customer_id BIGINT,
  status      VARCHAR,          -- open, pending, resolved
  subject     VARCHAR,
  opened_at   TIMESTAMPTZ
) FROM shop_pg TABLE 'public.tickets';

Step 3. Define the context as materialized views

Each view is one context object. The profile joins three inputs and aggregates across all of a customer's history; RisingWave updates the affected row when any input changes. The activity view is a hopping window that advances every minute. The status view keeps only the latest event per order. The embeddings view calls OpenAI once per new article version and stores a vector(1536); since it is append-only it can carry an HNSW index. The memory table is where the agent writes.

SQL (RisingWave)
-- Entity state: one row per customer, kept current incrementally.
CREATE MATERIALIZED VIEW customer_360 AS
SELECT c.customer_id, c.name, c.tier, c.region,
       COUNT(o.order_id) FILTER (WHERE o.status <> 'refunded') AS orders_total,
       COALESCE(SUM(o.amount) FILTER (WHERE o.status = 'paid'), 0) AS lifetime_value,
       COUNT(o.order_id) FILTER (WHERE o.status = 'refunded')     AS refunds_total,
       MAX(o.order_ts)                                            AS last_order_at,
       COUNT(t.ticket_id) FILTER (WHERE t.status <> 'resolved')   AS open_tickets
FROM customers c
LEFT JOIN orders  o ON o.customer_id = c.customer_id
LEFT JOIN tickets t ON t.customer_id = c.customer_id
GROUP BY c.customer_id, c.name, c.tier, c.region;

-- Recent activity: what this customer did in the last 15 minutes.
CREATE MATERIALIZED VIEW session_activity AS
SELECT customer_id,
       window_start, window_end,
       COUNT(*) FILTER (WHERE event_type = 'page_view')      AS page_views,
       COUNT(*) FILTER (WHERE event_type = 'add_to_cart')    AS cart_adds,
       COUNT(*) FILTER (WHERE event_type = 'checkout_error') AS checkout_errors,
       ARRAY_AGG(page ORDER BY event_ts DESC)                AS recent_pages
FROM HOP(clickstream, event_ts, INTERVAL '1 minute', INTERVAL '15 minutes')
GROUP BY customer_id, window_start, window_end;

-- Live status: the latest event per open order.
CREATE MATERIALIZED VIEW order_status_live AS
SELECT order_id, customer_id, amount, status, order_ts AS updated_at
FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY order_ts DESC) AS rn
  FROM orders
) t
WHERE rn = 1 AND status NOT IN ('delivered', 'refunded');

-- Retrievable knowledge: embed each article version as it is published.
-- The function is defined once so the model and key live in one place.
CREATE FUNCTION embed(VARCHAR) RETURNS VECTOR(1536) LANGUAGE SQL AS $$
  SELECT openai_embedding(
    '{"model": "text-embedding-3-small", "api_key": "sk-..."}'::jsonb, $1
  )::vector(1536)
$$;

CREATE MATERIALIZED VIEW article_embeddings AS
SELECT article_id, version, title, body, published_at,
       embed(title || E'\n' || body) AS embedding
FROM help_articles;

-- Vector indexes are built on append-only inputs; this view is one.
CREATE INDEX article_embeddings_hnsw ON article_embeddings
USING HNSW (embedding)
INCLUDE (article_id, version, title, body)
WITH (distance_type = 'cosine', m = 16, ef_construction = 64);

-- Agent memory: the agent writes here, views read it back.
CREATE TABLE agent_memory (
  memory_id    BIGINT PRIMARY KEY,
  customer_id  BIGINT,
  kind         VARCHAR,         -- promise, preference, escalation, summary
  content      VARCHAR,
  created_at   TIMESTAMPTZ
);

CREATE MATERIALIZED VIEW customer_memory AS
SELECT customer_id,
       ARRAY_AGG(kind || ': ' || content ORDER BY created_at DESC) AS notes
FROM agent_memory
WHERE created_at > NOW() - INTERVAL '90 days'
GROUP BY customer_id;

Step 4. Serve the context

Two queries per turn: one point lookup that returns the whole structured context as JSON, and one vector search for the question at hand. Both are plain SQL, so they work from any driver. The retrieval query uses the same embed function as the index, which keeps the query vector and the stored vectors in the same space.

SQL (RisingWave)
-- The whole structured context for one customer, from one snapshot.
SELECT to_jsonb(c)                         AS profile,
       to_jsonb(s)                         AS last_15_min,
       (SELECT jsonb_agg(to_jsonb(o))
          FROM order_status_live o
         WHERE o.customer_id = c.customer_id) AS open_orders,
       m.notes                             AS memory
FROM customer_360 c
LEFT JOIN session_activity s
       ON s.customer_id = c.customer_id
      AND s.window_end = (SELECT MAX(window_end) FROM session_activity
                           WHERE customer_id = c.customer_id)
LEFT JOIN customer_memory m ON m.customer_id = c.customer_id
WHERE c.customer_id = $1;

-- The three most relevant help articles for the customer's question,
-- latest version only, embedded with the same function as the index.
SELECT DISTINCT ON (article_id) article_id, title, body
FROM article_embeddings
ORDER BY article_id, version DESC, embedding <=> embed($1)
LIMIT 3;

Step 5. The agent loop

The agent code is short because the hard part moved into the views. It runs the two queries, hands the result to the model as a JSON block, and returns the answer. Any LLM client and any agent framework works; the context is plain data. Writing memory back is one INSERT into agent_memory, which the next turn's context query already reads.

Python (psycopg 3, Anthropic SDK)
import json, re, psycopg, anthropic

rw = psycopg.connect("postgresql://agent_ro@localhost:4566/dev", autocommit=True)
llm = anthropic.Anthropic()  # any LLM client works; the context is plain JSON

CONTEXT_SQL = open("context.sql").read()      # the first query above
RETRIEVE_SQL = open("retrieve.sql").read()    # the second query above

def bind(sql: str) -> str:
    """The SQL files use Postgres $n placeholders; psycopg wants %(name)s."""
    return re.sub(r"\$(\d+)", r"%(p\1)s", sql)

def build_context(customer_id: int, question: str) -> dict:
    with rw.cursor() as cur:
        cur.execute(bind(CONTEXT_SQL), {"p1": customer_id})
        profile, last_15_min, open_orders, memory = cur.fetchone()
        cur.execute(bind(RETRIEVE_SQL), {"p1": question})
        articles = [{"id": a, "title": t, "body": b} for a, t, b in cur.fetchall()]
    return {
        "profile": profile,
        "last_15_min": last_15_min,
        "open_orders": open_orders or [],
        "memory": memory or [],
        "relevant_articles": articles,
    }

def answer(customer_id: int, question: str) -> str:
    ctx = build_context(customer_id, question)
    resp = llm.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=600,
        system=(
            "You are a support agent. Use only the facts in <context>. "
            "The context is at most a few seconds old; treat it as current."
        ),
        messages=[{
            "role": "user",
            "content": f"<context>{json.dumps(ctx, default=str)}</context>\n\n{question}",
        }],
    )
    return resp.content[0].text

print(answer(42, "Where is my order and can I still change the address?"))

Alternative: let an MCP client read the views

For agents that run inside Claude Desktop, Cursor or another MCP host, skip the Python and point the RisingWave MCP server at the same read-only user. Its query tools let the model run the two queries above itself; its schema tools let it discover which context views exist. The server supports stdio, streamable HTTP and SSE transports.

MCP client configuration
{
  "mcpServers": {
    "risingwave": {
      "command": "python",
      "args": ["/path/to/risingwave-mcp/src/main.py"],
      "env": {
        "MCP_TRANSPORT": "stdio",
        "RISINGWAVE_CONNECTION_STR": "postgresql://agent_ro@localhost:4566/dev"
      }
    }
  }
}

Everything above is standard RisingWave SQL as documented in the vector indexes, AI functions and PostgreSQL CDC pages; the MCP server is at github.com/risingwavelabs/risingwave-mcp.

Alternatives

How else do teams give agents real-time data?

Every approach below works for some teams. The question is how many systems you want between an event and the agent, and whether the agent can get structured state and semantic retrieval from the same place.

ApproachHow it worksFreshnessTrade-off
Agent assembles context with tool callsThe agent calls the orders API, the CRM, the ticket system and a vector store on every turn and reasons over the raw results.Live, per callSlow and token-hungry; each call sees a different moment; every new context field is a new tool.
Flink or Kafka Streams plus a key-value cacheA stream job computes features and writes them to Redis or DynamoDB; the agent reads by key.SecondsFast reads, but no SQL at read time, no joins across cached objects, and a Java job plus a cache to operate.
Feature store (Tecton, Feast)Features are defined in Python, computed in batch or streaming, and served from an online store.Seconds to minutesBuilt for fixed-schema models; text retrieval, joins at read time and ad hoc context need other systems.
PostgreSQL plus pgvector plus cronNightly or hourly jobs aggregate operational tables and re-embed changed documents into pgvector.Minutes to hoursSimple and familiar, but context is stale between jobs and heavy aggregations compete with the production database.
Vector database plus batch embedding pipelineDocuments are embedded in a scheduled pipeline and upserted into Pinecone, Weaviate, Qdrant or Milvus.Per pipeline runStrong at large static corpora; no structured state, so the agent still needs a second source for live facts.
Confluent and Redpanda agent featuresConfluent exposes Flink-based streaming agents and Tableflow; Redpanda ships an MCP server and an agentic data plane over topics.SecondsTied to the streaming platform's cloud; context is still assembled by jobs the team writes and operates.
RisingWave (this page)Context objects, windows, embeddings and memory are materialized views over Kafka and CDC; agents read them via SQL or MCP.About one secondOne system to run; vector indexes require append-only inputs; very large static corpora are better served by a dedicated vector store.

Design Decisions

Six questions to settle before you build

What belongs in a view and what should the agent compute at read time?

Put anything that is expensive, shared across turns, or needs a consistent snapshot into a view: aggregates, joins, windows, embeddings. Leave per-turn filtering (which three articles, which open order the question is about) to the read query. A good test: if two agent turns in the same minute would compute the same thing, it belongs in a view.

How do I keep the context inside a token budget?

Shape the view for the prompt, not for a dashboard. Return counts and the latest few items (ARRAY_AGG with ORDER BY and a slice) rather than full histories, cast to JSON in SQL so the agent code does no reshaping, and keep retrieval at LIMIT 3 to 5. In the reference implementation the whole context is typically under 1,500 tokens.

Embed in the stream or in a batch job?

Embed in the stream when documents change often or when a stale answer is costly (pricing, policy, outage notices). Batch is fine for a static corpus. In RisingWave the cost is one embedding API call per new document version, made once, with retries and Prometheus metrics built into openai_embedding.

When should a window use EMIT ON WINDOW CLOSE?

Use the default (emit on every change) for context the agent reads live, such as the last-15-minutes activity view, so the current window is visible. Use EMIT ON WINDOW CLOSE for windows that feed downstream systems or memory, where you want one final value per window instead of a stream of updates.

How does agent memory fit?

Treat memory as another event stream. The agent INSERTs into agent_memory; a view summarises it per customer; the context query joins it in. Because memory and live data live in one system, you can also check memory against reality, for example a view that flags promises whose related order did not ship.

How do I lock this down?

Give the agent a read-only database user with SELECT on the context views only, never on raw sources. Keep write access limited to the agent_memory table. If the agent uses the MCP server, run it with that same read-only connection string so the 100+ management tools cannot change the pipeline.

When Not to Use This

Three cases where a streaming context layer is overkill

  • →A one-shot RAG application over a static document set with no live operational data. A vector database or pgvector with a batch embedding job is simpler.
  • →Training-time feature pipelines where point-in-time correctness for historical labels is the main requirement. A feature store with offline and online stores is built for that.
  • →Agents that need sub-10-millisecond p99 point lookups at very high QPS. Keep the views in RisingWave, but sink them to Redis or a similar store for serving.

Frequently Asked Questions

What is real-time context for AI agents?
How is context engineering different from RAG?
How do you give an AI agent access to real-time data?
What is a streaming feature store, and do agents need one?
Can an AI agent query RisingWave directly?
How do you keep RAG embeddings fresh?
What is the difference between agent memory and agent context?
Do I still need a separate vector database?
What latency can an agent expect from this architecture?

Give your agent context it can trust

Run the reference implementation on RisingWave Cloud or a single Docker container.

Start Free
The engine for real-time streaming data.
Analyze, search, and decide.
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.