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.
- Install
- Your first memory
- The agent loop
- Attach to a framework
- Persist it
- Tell it things directly
- Let a model manage memory
- Run it as a service
- MCP hosts
- Look inside: the console
- Keep it healthy
- Production checklist
- Configuration reference
- 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, andrecall()retrieves every kind with fixed caps. You can switch at any time withMemoryLayer(..., 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_memorydecorator; 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_KEYset,/v1and/mcprequireAuthorization: Bearer <key>orX-API-Key. - Without a key, the server refuses to listen on a non-loopback address (unless you pass
--insecure), and it checksHostandOriginheaders. - 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:
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,AgnoEmbedderor a function). After switching, runmemory.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 withdry_run, and watch thememory.managedevents. - Secrets and PII.
DropSecretsis on by default: passwords, API keys, tokens, private keys and card numbers are never stored, whoever proposes them. AddRedactPIIto mask e-mails and phone numbers. Users can ask the assistant to forget things, andmemory.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-hostbehind a proxy. - Several workers. The context cache lives in each process for
context_cache_ttlseconds (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 withWebhookSink, or pollGET /v1/events.memory.stats()andGET /v1/intelligencehave counters. - Python 3.14. Annotations are evaluated lazily (PEP 649).
attach()and@with_memorynever 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 abovemin_importance. Usecapture="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 withkioko.intelligent(). Previews in the Intelligence tab work either way.