Skip to content

Kioko guide: add memory to your agent

This guide takes a new user from installation to a production setup. It follows one realistic project throughout: a plant support assistant for controls engineers that remembers who each engineer is, what they are working towards, which procedures fix which faults, what the team decided, and what worked.

  1. Install
  2. Your first memory
  3. The agent loop
  4. Attach to a framework
  5. Persist it
  6. Tell it things directly
  7. Let a model manage memory
  8. Run it as a service
  9. MCP hosts
  10. Look inside: the console
  11. Keep it healthy
  12. Production checklist
  13. Configuration reference
  14. Troubleshooting

The other documents go deeper: memory types covers each of the 15 kinds, intelligence the self-managing mode, stores SQLite, pgvector and Qdrant, console the web UI, and the cookbook has runnable recipes with their output.


1. Install

Kioko needs Python 3.14 or newer. The core has no dependencies: storage, the offline embedder, the extraction rules, the intelligence, the REST and MCP server and the web console all use the standard library.

pip install kioko                 # the core
pip install "kioko[pgvector]"     # + Postgres / pgvector (psycopg)
pip install "kioko[agno]"         # + the Agno adapter
pip install .                     # or from the source folder

On Windows, use the launcher: py -3.14 -m pip install .... To check the installation, run kioko demo. It runs a scripted conversation with the offline demo agent.


2. Your first memory (five lines)

import kioko

memory = kioko.intelligent("sqlite:///plant-memory.db")      # a file next to your code; nothing else to set up

# After each exchange, hand the turn to Kioko. It decides what is worth keeping, and where.
memory.record_turn("Hi, I'm Asha, a controls engineer at the Pune plant. I prefer short answers.",
                   "Nice to meet you, Asha!", user_id="asha", session_id="mon-shift")
memory.record_turn("My goal is to get line 3 OEE above 85% by December 31.",
                   "Noted - let's track it.", user_id="asha", session_id="mon-shift")
memory.record_turn("To reset the safety relay, switch off the main breaker, then press reset for 5 seconds, "
                   "then switch the breaker back on.", "Thanks, saved.", user_id="asha", session_id="mon-shift")

# Before each answer, ask Kioko what the agent should know.
ctx = memory.recall("The safety relay tripped again - what do I do?", user_id="asha", session_id="tue-shift")
print(ctx.text)

Output:

<memory>
Long-term memory about the user. Use it to personalise your answer. If it conflicts with the user's latest message, trust the latest message. Do not mention this block unless asked.

Instructions from the user:
- User prefers short answers.

About the user:
- User is a controls engineer.
- User works at the Pune plant.
- User's name is Asha.

The user's open goals:
- Get line 3 OEE above 85% by December 31 (medium, 0%, due 2026-12-31)

Procedures that may apply:
- reset the safety relay: 1) Switch off the main breaker; 2) Press reset for 5 seconds; 3) Switch the breaker back on
</memory>

Each part of the conversation went to the kind of memory where it belongs:

  • name, role and employer became profile slots;
  • "I prefer short answers" is about how to answer, so it became a standing instruction;
  • the goal became a goal with a due date;
  • the how-to became a procedure with ordered steps.

The question the next day was recognised as a how-to, so the procedure was retrieved first. Nothing was configured, and no model was called. That is kioko.intelligent(): see intelligence.

Prefer full control? kioko.MemoryLayer.sqlite("plant-memory.db") gives you the same memory without the self-management. learn() then extracts with rules, and recall() retrieves every kind with fixed caps. You can switch at any time with MemoryLayer(..., intelligence=True).


3. The agent loop

Whatever your agent looks like, memory takes three lines per turn: recall → answer → record.

import kioko

memory = kioko.intelligent("sqlite:///plant-memory.db")
user, session = "asha", "tue-shift"


def call_llm(system: str, user_message: str) -> str:
    """Stand-in for your model call (OpenAI, Azure OpenAI, Anthropic, a local model ...)."""
    return f"[model answer, using {system.count(chr(10) + '- ')} memory lines]"


def answer(message: str) -> str:
    ctx = memory.recall(message, user_id=user, session_id=session)                       # 1. remember
    reply = call_llm("You are a plant support assistant.\n\n" + ctx.text, message)       # 2. answer
    memory.record_turn(message, reply, user_id=user, session_id=session)                 # 3. learn
    return reply


for message in ["The safety relay on line 3 tripped again, ticket INC-7731.",
                "That worked, thanks!",
                "We switched the line 3 HMI from PanelView to FactoryTalk Optix because of remote access.",
                "I'm about 40% of the way to the OEE target."]:
    print(f"asha  > {message}\nagent > {answer(message)}")

print("\nworking memory:", memory.working(session, user_id=user).snapshot())
print("goal:", memory.goals.active(user_id=user)[0].line())
print("procedure track record:", memory.procedures.list(user_id=user)[0].successes, "success")
print("decisions:", [d.line() for d in memory.decisions.log(user_id=user)])

Output (after the five-line example above):

asha  > The safety relay on line 3 tripped again, ticket INC-7731.
agent > [model answer, using 6 memory lines]
asha  > That worked, thanks!
agent > [model answer, using 6 memory lines]
asha  > We switched the line 3 HMI from PanelView to FactoryTalk Optix because of remote access.
agent > [model answer, using 8 memory lines]
asha  > I'm about 40% of the way to the OEE target.
agent > [model answer, using 6 memory lines]

working memory: {'ticket': 'INC-7731'}
goal: Get line 3 OEE above 85% by December 31 (medium, 40%, due 2026-12-31)
procedure track record: 1 success
decisions: ['line 3 HMI: chose FactoryTalk Optix because of remote access (accepted 2026-10-27)']

Without being asked, Kioko:

  • kept the ticket number in the session's working memory;
  • credited "that worked" to the procedure the agent had just used;
  • logged the HMI decision with its reason;
  • moved the OEE goal to 40%.

With a real model, call_llm is a few lines. For example, with Azure OpenAI:

import os
from openai import AzureOpenAI

client = AzureOpenAI(azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], api_version="2024-10-21")
# key: AZURE_OPENAI_API_KEY, or azure_ad_token_provider=... for Entra ID

def call_llm(system: str, user_message: str) -> str:
    reply = client.chat.completions.create(model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
                                           messages=[{"role": "system", "content": system},
                                                     {"role": "user", "content": user_message}])
    return reply.choices[0].message.content

To get the most from provider prompt caching, use memory.kv.openai_messages(...) or memory.kv.anthropic_system(...). They put the stable part of the memory first, byte-identical from turn to turn (see KV-cache memory).


4. Attach to a framework instead

If your agent is built with a framework, attach() adds the same loop in one line. It recalls before each run and records after it:

from kioko import attach

agent = attach(agent, memory)                         # Agno agent: hooks + memory tools
graph = attach(graph, memory)                         # LangChain runnable / LangGraph graph (user and thread from config)
crew = attach(crew, memory, input_key="topic")        # CrewAI crew: which input holds the user's text
answer = attach(my_function, memory)                  # any function: answer(message, user_id=..., session_id=...)

It works with Agno, LangChain, LangGraph, CrewAI, the OpenAI Agents SDK, AutoGen, smolagents, Semantic Kernel, your own classes and plain functions. For more control there are building blocks:

  • the @with_memory decorator;
  • MemoryMiddleware(memory).before(...) / .after(...);
  • memory.inject(messages, user_id=...) for OpenAI-style message lists;
  • memory.tools(user_id=...): save / search / update / delete tools your agent can call itself.

Attach to any agent has framework-specific notes, and examples/ has runnable examples (02_agno_agent.py, 03_any_agent.py, 05_frameworks_attach.py).


5. Persist it

The memory lives wherever the URL says. Change one string and the code stays the same:

memory = kioko.intelligent("sqlite:///plant-memory.db")                         # one file
memory = kioko.intelligent("postgresql://kioko:<password>@db:5432/kioko")      # Postgres + pgvector
memory = kioko.intelligent("qdrant://qdrant:6333/plant_memories")              # Qdrant
memory = kioko.connect("http://memory-service:8765")                           # a shared Kioko service

docker compose -f docker-compose.stores.yml up -d starts Postgres with pgvector, Qdrant and a Kioko service locally. Stores covers set-up, indexes, switching embedders (memory.reembed()) and writing your own store.

Use a semantic embedder in production. The default one is offline and lexical: it matches words, not meaning.

from kioko import AzureOpenAIEmbedder, OpenAIEmbedder

memory = kioko.intelligent("postgresql://...", embedder=AzureOpenAIEmbedder(deployment="text-embedding-3-small"))
memory = kioko.intelligent("postgresql://...", embedder=OpenAIEmbedder(model="text-embedding-3-small"))
memory = kioko.intelligent("postgresql://...", embedder=lambda texts: my_model.encode(texts).tolist())

6. Tell it things directly

Conversation fills memory by itself. When your code knows something, write it to the right kind directly. For example, to load the plant's runbooks into shared knowledge, or to record a decision from a change ticket:

memory.semantic.ingest(open("runbooks/line3.md").read(), source="line3-runbook", topic="operations")  # shared
memory.procedures.add("Recalibrate the line 3 encoder", user_id="asha",
                      steps=["Stop the line", "Run the calibration wizard", "Verify 10 cycles"])
memory.decisions.record("Historian", chosen="InfluxDB", rationale="retention policies", user_id="asha")
memory.goals.add("Cut changeover time to 15 minutes", user_id="asha", priority="high", due="2027-03-31")
memory.graph.add("Asha", "reports_to", "Ravi", user_id="asha")
memory.procedures.add_rule("Always cite the ticket number", user_id="asha")       # a standing instruction

Memory types describes all fifteen kinds and their APIs. Each kind has a runnable recipe in the cookbook.


7. Let a model manage memory

The offline intelligence covers common phrasings in English. For open-ended conversations, other languages, or subtler updates, give it a model. One call per turn decides every memory operation. Kioko still skips trivial turns without a call, validates what the model proposes, drops secrets, and falls back to the offline rules if the model fails.

def ask(prompt: str) -> str:                                   # any prompt -> text function
    reply = client.chat.completions.create(model=os.environ["AZURE_OPENAI_DEPLOYMENT"], temperature=0,
                                           response_format={"type": "json_object"},
                                           messages=[{"role": "user", "content": prompt}])
    return reply.choices[0].message.content

memory = kioko.intelligent("postgresql://...", llm=ask)        # routing, reflection and summaries use the model

Before you rely on it, preview what it decides:

report = memory.manage("Downtime is already down 12%.", user_id="asha", dry_run=True)
print(report.summary())       # goal.progress "Cut unplanned downtime on line 3 by 20%" -> 60%

Intelligence covers how both modes decide, the operations, the retrieval plans, maintenance, measured savings and the research behind it.

How memories connect

With an intelligence (or auto_link=True), every memory is linked to related memories as it is written, and recall() follows the links. You don't have to do anything; it helps to know how to look:

for link in memory.graph.links(memory_id):
    print(link.line(), "-", link.why)
# decision "use pgvector because we already run Postgres" serves goal "Ship the Kioko memory layer ..." - via Kioko uses pgvector

memory.graph.related(memory_id)          # what is connected, best first, with the chain of links
memory.graph.connect(user_id="asha")     # link memories written before linking was on (once, after an import)

When a user tells the agent a fix worked, the fix is linked to the problem it solved; the next time the problem comes up, recall brings the fix along. The Graph tab of the console draws it all. Graph memory describes the algorithm.


8. Run it as a service (for any language)

When agents in several processes, containers or languages should share one memory, run it as a service. --intelligence makes the service manage memory by itself:

export KIOKO_API_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
kioko serve --store postgresql://kioko:<password>@db:5432/kioko --intelligence --host 0.0.0.0 --port 8765
REST API   http://localhost:8765/v1        (OpenAPI document: /v1/openapi.json)
MCP        http://localhost:8765/mcp

The agent loop is the same in every language: POST /v1/recall before the model call, POST /v1/turns after it.

H="Authorization: Bearer $KIOKO_API_KEY"
curl -s localhost:8765/v1/recall -H "$H" -H 'Content-Type: application/json' \
  -d '{"user_id": "asha", "session_id": "s1", "query": "What should I focus on this week?", "budget_tokens": 600}'
curl -s localhost:8765/v1/turns -H "$H" -H 'Content-Type: application/json' \
  -d '{"user_id": "asha", "session_id": "s1", "user_message": "My goal is to get line 3 OEE above 85% by December 31.",
       "assistant_message": "Noted."}'

The responses, abridged:

{"plan": {"intents": ["goals", "strategy"], ...}, "tokens": 74,
 "text": "<memory>\n...\nThe user's open goals:\n- Get line 3 OEE above 85% by December 31 (medium, 0%, due 2026-12-31)\n</memory>", ...}

{"results": [{"action": "added", "memory": {"memory_type": "goal",
              "content": "Goal: Get line 3 OEE above 85% by December 31.", ...}, ...}]}
Endpoint Purpose
POST /v1/recall Everything the agent should know for a query, as a prompt-ready text plus the sections; hybrid, planned, within budget_tokens
POST /v1/turns After a reply: store the exchange and learn from it (managed by the intelligence when it is on)
POST /v1/manage What the intelligence decides for a turn (dry_run to preview)
POST /v1/plan · POST /v1/maintain · GET /v1/intelligence Recall plans; reflect / pin / consolidate now; intelligence status and counters
POST /v1/context · POST /v1/search · POST /v1/learn The lower-level read, search and extraction calls
POST /v1/kinds/{kind}/{operation} · GET /v1/kinds Any typed operation, e.g. goals/add, decisions/supersede, graph/neighbors
GET /v1/graph · POST /v1/consolidate · GET /v1/cache The knowledge graph, maintenance, cache statistics
GET /v1/pins · POST/DELETE /v1/memories/{id}/pin Pinned (always-at-hand) memories
POST /v1/memories · GET · GET/PATCH/DELETE /v1/memories/{id} · DELETE /v1/memories?user_id= Create, list, read, edit, delete; clear one user
POST /v1/messages · GET /v1/sessions · GET /v1/sessions/{id}/messages · POST .../summarize · POST .../end · DELETE /v1/sessions/{id} Sessions and history
GET /v1/events?after=<seq> · GET /v1/users · GET /v1/stats · GET /v1/info Activity log, users, counts, configuration
GET /v1/health · GET /v1/openapi.json Liveness and the OpenAPI document (no key needed)

The OpenAPI document (kioko openapi --server-url https://memory.example.com > openapi.json) imports into Postman, Swagger UI, Azure API Management or a Power Platform custom connector.

From Python, kioko.connect("http://memory-service:8765") returns a RemoteMemory with the same API as a local memory: every kind, recall, manage, plan, pin .... It reads the API key from KIOKO_API_KEY. examples/rest_client.mjs is a Node client, and the cookbook has a Python one.

Security.

  • With KIOKO_API_KEY set, /v1 and /mcp require Authorization: Bearer <key> or X-API-Key.
  • Without a key, the server refuses to listen on a non-loopback address (unless you pass --insecure), and it checks Host and Origin headers.
  • One key grants access to every user's memories. Put a gateway in front for per-user authentication, and use HTTPS outside a private network.

9. MCP hosts

The same memory works as tools for Claude Desktop and Claude Code, VS Code with GitHub Copilot, Cursor, Copilot Studio and any other MCP client. The tools are:

  • get_memory_context, search_memory, list_memories, explore_entity;
  • save_memory, learn_from_conversation, manage_goal, record_decision, record_lesson, relate_entities;
  • update_memory, delete_memory.

The server decides whose memory the tools use, never the model.

{ "mcpServers": { "kioko": { "command": "kioko", "args": ["mcp", "--user", "asha", "--intelligence"] } } }

That is the stdio configuration for Claude Desktop or Cursor; memories go to ~/.kioko/memory.db. Over HTTP (VS Code .vscode/mcp.json, Copilot Studio), use https://<host>/mcp with the X-Kioko-User and Authorization headers. The README has the full configurations.


10. Look inside: the console

kioko ui --db plant-memory.db --intelligence --open        # http://127.0.0.1:8765
kioko ui --url http://memory-service:8765                   # manage a running service from your laptop

The console shows every user and session. You can chat with an agent and see what it recalled, injected and learned on each turn. You can browse, edit, pin and delete memories; inspect every kind, the graph and the working memory; preview the intelligence's decisions; plan and run recalls; and follow all activity live. Console explains how to reach it on a server (an SSH tunnel or --url) and how to keep it safe.


11. Keep it healthy

With intelligence on, maintenance runs by itself:

  • sessions are summarised;
  • recent episodes are reflected into insights;
  • hot memories are pinned;
  • sessions are wrapped up when a user moves on;
  • duplicates are merged and faded episodes forgotten.

For stores that several processes share, also run it nightly:

kioko maintain --store "$KIOKO_STORE_URL"           # every user: reflect, pin hot memories, consolidate
memory.maintain()                                    # the same from Python (POST /v1/maintain over REST)
memory.end_session("tue-shift", user_id="asha")      # when your app knows a session is over

Keep each context small with budget_tokens= on recall(). The default is 1200. Items that do not fit are listed in ctx.omitted, and the session summary is shortened rather than dropped.


12. Production checklist

  • Embeddings. Use a semantic embedder (AzureOpenAIEmbedder, OpenAIEmbedder, AgnoEmbedder or a function). After switching, run memory.reembed().
  • Store. Use Postgres + pgvector or Qdrant for many processes and large memories, and SQLite for one machine. Back the database up like any other.
  • Intelligence. Offline is a good start. Add llm= for open-ended conversations and for languages other than English, because the rule-based extraction is English-only. Preview with dry_run, and watch the memory.managed events.
  • Secrets and PII. DropSecrets is on by default: passwords, API keys, tokens, private keys and card numbers are never stored, whoever proposes them. Add RedactPII to mask e-mails and phone numbers. Users can ask the assistant to forget things, and memory.clear(user_id=...) / DELETE /v1/memories?user_id= delete a user's data.
  • Service. Set KIOKO_API_KEY, terminate TLS in front, keep the console off the internet, and use --allowed-host behind a proxy.
  • Several workers. The context cache lives in each process for context_cache_ttl seconds (15). Lower it, or set it to 0 when several workers write the same users' memories.
  • Observability. Every write, recall and decision is an event. Subscribe with memory.subscribe(...), forward with WebhookSink, or poll GET /v1/events. memory.stats() and GET /v1/intelligence have counters.
  • Python 3.14. Annotations are evaluated lazily (PEP 649). attach() and @with_memory never force them.

13. Configuration reference

MemoryLayer(...) (and kioko.intelligent(...), which passes these through):

Option Default Meaning
store in-process where memories live (see stores)
embedder HashingEmbedder() text → vector; any Embedder or a function
extractor RuleBasedExtractor() conversation → memory candidates (LLMExtractor(llm) for open-ended text)
intelligence None True, Intelligence(llm=...) or any object with manage()
processors [DropSecrets()] write pipeline: drop or transform candidates
summarizer ExtractiveSummarizer() session summaries (LLMSummarizer(llm))
dedup_threshold / contradiction_threshold 0.9 / 0.8 when a write reinforces, or supersedes, an existing memory
auto_graph False (True in intelligent()) extract graph relations from conversation
embedding_cache / context_cache_ttl 4096 / 15 s caches (0 disables)
tokenizer ~4 characters per token token counting for budgets
history_store / event_store the store, if it supports them where session history and events go

Intelligence(...) options: llm, capture ("rules", "selective" or "all"), min_importance (0.4), plan_with_llm, background, and the scheduler's summarize_every (10), reflect_after (2.5), pin_threshold (5), max_pinned (5), pin_every (10) and consolidate_every (200). See intelligence.

Environment variables:

Variable Used by
KIOKO_API_KEY the service's key, and RemoteMemory's default
KIOKO_URL connect() / attach() / CLI commands when no target is given
KIOKO_DB the default SQLite path for the CLI
KIOKO_PG_DSN (or DATABASE_URL) PgVectorStore() without a DSN
QDRANT_API_KEY QdrantStore() without api_key=
KIOKO_USER the default user of kioko mcp

CLI (every command takes --db PATH, --store URL or --config JSON; most take --url for a service; and --intelligence / --intelligence-llm MODULE:FUNCTION):

kioko serve | ui | mcp                 run the service / the console / an MCP server over stdio
kioko recall "question" --user U       the block an agent would get   (--session, --budget, --plan, --json)
kioko manage "text" --user U           what the intelligence decides   (--reply, --session, --dry-run, --json)
kioko plan "question" --user U         which kinds a question needs
kioko maintain [--user U]              reflect, pin hot memories, consolidate   (--tasks reflect,pin,consolidate)
kioko kinds | list | search | add | context | consolidate | components | openapi | demo

14. Troubleshooting

  • "Nothing to remember (only questions or requests)." The gate skips turns that hold no information, such as questions and requests to the assistant. That is intended. Say it as a statement ("I'm based in Pune"), or use memory.add(...) / memory.facts.add(...).
  • A statement was not kept. Look at the report: memory.manage(text, user_id=..., dry_run=True).summary(). Unrecognised statements are kept only above min_importance. Use capture="all", add an extraction rule (RuleBasedExtractor(extra_rules=[Rule(...)])), or give the intelligence a model.
  • Recall misses something obvious. With the default lexical embedder, "car" does not match "automobile". Switch to a semantic embedder and run memory.reembed(). To see why each memory was or was not recalled, use the console's Recall tab (signals and scores).
  • pgvector: "permission denied to create extension vector". Have an administrator run CREATE EXTENSION vector; once in the database.
  • Qdrant: wrong vector size. A collection has one size. Use a new collection= after changing embedders.
  • The console says "Not self-managing". Start it with --intelligence, or open a memory built with kioko.intelligent(). Previews in the Intelligence tab work either way.