Skip to content

attach(): any agent, one line

from kioko import MemoryLayer, attach

memory = MemoryLayer.sqlite("memory.db")    # or kioko.intelligent("sqlite:///memory.db") to let it manage itself

# LangChain runnable or LangGraph graph: user and thread come from the config
chain = attach(prompt | llm, memory)
chain.invoke({"input": "I prefer short answers"},
             config={"configurable": {"user_id": "kumar", "thread_id": "t1"}})

# Agno agent: hooks + memory tools
agent = attach(Agent(model=OpenAIChat(id="gpt-4o-mini")), memory)
agent.run("What do you know about me?", user_id="kumar")

# CrewAI crew: say which input holds the user's text
crew = attach(my_crew, memory, input_key="topic", user_id="kumar")

# OpenAI Agents SDK, or anything else: wrap the call in a function
from agents import Agent, Runner
support = Agent(name="Support", instructions="You are a helpful assistant.")
ask = attach(lambda message: Runner.run_sync(support, message).final_output, memory)
ask("I prefer metric units", user_id="kumar")

attach(target, memory=None, *, adapter=None, **options) picks the first adapter whose matches() accepts the target, and returns what to use from now on (usually the same object):

Adapter Targets What it does
agno Agno Agent / Team Pre-hook recalls memories into the input, post-hook learns, plus save/search/update/delete_memory tools scoped to run_context.user_id.
method Any object with invoke/ainvoke, stream/astream, run/arun, chat/achat, ask/aask, query/aquery, kickoff/kickoff_async: LangChain runnables, LangGraph graphs, CrewAI crews, your classes Wraps those methods in place (same object, same type). Objects that refuse new attributes get a MemoryProxy. methods=[...] picks the methods.
callable Functions and callable objects, sync, async or streaming Returns a wrapped function (with_memory).
yours Anything Registered adapters and installed plug-ins are tried first (see Extend).

What is injected, and where. The memory block goes where the agent will read it, keeping the input's shape:

Input shape Where the memory block goes
a string in front of the text
a message list (OpenAI dicts, LangChain messages, ("human", "hi") tuples) a system message after your own system messages
a dict with messages (LangGraph state, chat-completion requests) the same, inside messages
a dict with input/query/question/prompt/message/... in front of that field (input_key= picks another field)
a pydantic model or dataclass with such a field (LangChain prompt values too) on a copy
one message with multi-part content (vision) a leading text part

Who is talking. The user and session ids come from user_id= / session_id= / thread_id= arguments (removed before the call unless the target declares them), then a LangChain/LangGraph config={"configurable": {...}} (or config["metadata"]), then the payload dict itself. Without one, user_id= and session_id= given to attach() are the defaults. scope=lambda args, kwargs: ... decides first; it receives the call's original arguments, and if it returns None the rules above apply.

Replies. Replies are read from strings, {"output"|"answer"|"content": ...}, LangGraph state (the last assistant message), LangChain messages, Agno RunOutput, OpenAI Agents RunResult.final_output and CrewAI CrewOutput.raw. Streams (generators and async generators) pass through untouched and are recorded when they finish.

Nested calls. When a wrapped method calls another wrapped method of the same object (chat() → run(), or stream() → invoke(), even through a thread pool), memory is added once and the turn is recorded once. Async streams fetch memory off the event loop. If a stream fails, the agent's own error is raised, never a recording error.

detach(agent) undoes attach() and restores the original methods. Calling attach() again replaces the previous wrapping, so the newest options win. Attach once at startup and pass user_id with each call; don't re-attach per request. attach(agent, "http://memory:8765"), or attach(agent) with KIOKO_URL set, connects to a shared service instead of a local layer.

Checkpointed threads. For LangChain and LangGraph targets the injected system message carries a stable id (kioko-memory), so a checkpointed LangGraph thread replaces last turn's memory message instead of piling up a new one each turn. Memory blocks are never stored as history or learned from, even when they come back inside a later input.

Agno in detail

from kioko.integrations import AgnoMemory

mem = AgnoMemory(memory, inject="input", background=False)   # what attach() uses for Agno agents
agent = Agent(model=..., tools=mem.tools(), pre_hooks=[mem.pre_hook], post_hooks=[mem.post_hook])
Piece What it does
pre_hook Retrieves the user's instructions, profile facts and the memories relevant to the message, and puts the memory block in front of run_input.input_content.
post_hook Stores the exchange as session history, extracts and writes new memories, then restores the original input in the stored run, so memory blocks don't pile up in Agno's history.
tools save_memory, search_memory, update_memory, delete_memory. They declare run_context, so Agno injects the current run (hidden from the model) and the tools act on run_context.user_id.

Options: agent_id, inject="input" | "none", learn, tools, allow_update, allow_delete, context_limit, background (learn in a worker thread), restore_input. Also: LLMExtractor(llm=agno_agent), AgnoEmbedder(any_agno_embedder), and export_to_agno / import_from_agno to sync with Agno's built-in user memories. The adapter follows Agno's documented 3.x API (2.2.7+). Run python -m unittest tests.test_agno_contract -v once with Agno installed to check it against the real package.

Lower-level building blocks

from kioko import MemoryMiddleware, with_memory

@with_memory(memory)                                     # sync, async or streaming functions
def my_agent(message: str, memory_context: str = "") -> str:
    return call_llm(system=memory_context, user=message)

hooks = MemoryMiddleware(memory, agent_id="support-bot")  # inside a graph node, a task, your own loop
ctx = hooks.before(question, user_id="kumar", session_id="s1")   # str(ctx) is the prompt block
reply = run_my_agent(question, context=str(ctx))
hooks.after(question, reply, user_id="kumar", session_id="s1")   # history + learning

tools = memory.tools(user_id="kumar")                    # OpenAI / Azure OpenAI function calling
messages = memory.inject([{"role": "user", "content": question}], user_id="kumar")
response = client.chat.completions.create(model="gpt-4o-mini", messages=messages,
                                          tools=tools.openai_schemas())
for call in response.choices[0].message.tool_calls or []:
    output = tools.call(call.function.name, call.function.arguments)

tools.functions() returns plain functions with type hints and docstrings, ready to register as tools in LangChain (StructuredTool.from_function), CrewAI, smolagents, AutoGen or the OpenAI Agents SDK (function_tool). The tools only read and change the memories of the user they are bound to.