Skip to main content
doc0blade47/mem0documentation home
Sign in

Python Agent Core

Last updated Oct 11, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The Python Agent Core provides the shared runtime foundation for Mem0 coding agent plugins, managing local evidence persistence, hook orchestration, and background synchronization. It solves the challenge of capturing developer interactions across distinct agent harnesses by maintaining an SQLite-backed evidence store for session events and subagent turns. Design decisions prioritize local-first reliability through detached worker handoffs, robust error recovery, and strict data scoping.

Sources: integrations/agent-plugin-core/python/hook_runner.py:1-34, integrations/agent-plugin-core/python/memory_core.py:1-8, integrations/agent-plugin-core/python/flush_worker.py:1-6

Evidence Store Architecture and Session State

Storage Model and Session Synchronization

The underlying SQLite-backed storage model captures session events, subagent turns, and prompt evidence locally before synchronization. When agents compact or conclude sessions, the system evaluates the state via flush_session(), verifying API keys, determining repository contexts through store.repo_for_session(), and preparing pending packets via store.prepare_flush() to synchronize records with the platform.

Sources: integrations/agent-plugin-core/python/memory_core.py:1962-1978, integrations/mem0-agent-plugin/core/memory_core.py:1962-1978, integrations/antigravity-plugin/core/memory_core.py:1962-1978, integrations/cursor-plugin/core/memory_core.py:1962-1978, integrations/codex-plugin/core/memory_core.py:1962-1978

Note

Extraction jobs are durable server-side. Transient polling failures during _wait_for_event() do not discard jobs that may still complete normally.

Sources: integrations/agent-plugin-core/python/memory_core.py:1925-1929, integrations/mem0-agent-plugin/core/memory_core.py:1925-1929, integrations/antigravity-plugin/core/memory_core.py:1925-1929, integrations/cursor-plugin/core/memory_core.py:1925-1929, integrations/codex-plugin/core/memory_core.py:1925-1929

Public API Surface and Event Polling

The public functions manage background heartbeats and remote event tracking. The function touch_handoff_heartbeat() updates the worker's handoff file timestamp using MEM0_CODE_HANDOFF_PATH to prevent recovery mechanisms from prematurely relaunching active workers. Meanwhile, _wait_for_event() polls server-side extraction tasks using configurable parameters.

Parameter / VariableEnvironment VariableDefault ValuePurpose
Extraction Wait TimeoutMEM0_CODE_EXTRACTION_WAIT_SECONDS"120"Maximum seconds to wait for remote event extraction to complete.
Event Poll IntervalMEM0_CODE_EVENT_POLL_SECONDS"1" (minimum 0.1)Delay interval between successive event status polling checks.
Worker Handoff PathMEM0_CODE_HANDOFF_PATH"" (disabled)Filesystem path for the worker handoff heartbeat file.
Platform API URLMEM0_API_URLDEFAULT_API_URLBase URL endpoint for Mem0 platform synchronization requests.

Sources: integrations/agent-plugin-core/python/memory_core.py:1893-1910, integrations/mem0-agent-plugin/core/memory_core.py:1893-1910, integrations/antigravity-plugin/core/memory_core.py:1893-1910, integrations/cursor-plugin/core/memory_core.py:1893-1910, integrations/codex-plugin/core/memory_core.py:1893-1910

Hook Runner Orchestration and Injection

Input Handling and First-Prompt Context Injection

The hook runner reads JSON payloads from standard input via read_hook_input(), safely parsing dictionary structures while falling back to empty dictionaries on decoding or operating system errors.

def read_hook_input() -> dict:
    try:
        value = json.load(sys.stdin)
        return value if isinstance(value, dict) else {}
    except (json.JSONDecodeError, OSError):
        return {}
 

Sources: integrations/agent-plugin-core/python/hook_runner.py:44-50

When a user submits a prompt, first_prompt_memory_output() determines whether the interaction represents the session's initial prompt via record_user_prompt(). If it is the first prompt, the function checks environment constraints before executing a memory search.

def first_prompt_memory_output(store: EvidenceStore, hook_input: dict) -> dict:
    """Search once before the agent handles the first prompt in a session."""
    repo, session_id, prompt, is_first_prompt = record_user_prompt(store, hook_input)
    if not is_first_prompt:
        return {}
    try:
        minimum_query_chars = int(os.environ.get("MEM0_CODE_MIN_QUERY_CHARS", "20"))
    except ValueError:
        minimum_query_chars = 20
    if len(prompt.strip()) < max(minimum_query_chars, 1):
        return {}
    result = search_memories(
        store, repo, session_id, bounded(prompt, 6000),
        top_k=5, operation="first-prompt-search", timeout=2,
    )

Sources: integrations/agent-plugin-core/python/hook_runner.py:62-76

Note

The search query character length must meet or exceed the minimum threshold defined by MEM0_CODE_MIN_QUERY_CHARS (defaulting to 20 characters) and is bounded to a maximum of 6000 characters before querying.

Sources: integrations/agent-plugin-core/python/hook_runner.py:67-74

If matching memories are retrieved, they are formatted into additional context and logged via telemetry under the context_injected event trigger. The hook runner then returns a dictionary containing hookSpecificOutput with the populated additionalContext.

if not result.memories:
    return {}
context = format_context(
    result.memories,
    "Mem0 found these relevant memories from earlier work in this repository:",
)
telemetry.record(
    "context_injected",
    repo=repo, session_id=session_id, trigger="first-prompt",
    memory_count=len(result.memories), context_chars=len(context),
    prompt_chars=len(prompt),
)
return {
    "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": context,
    },
}

Sources: integrations/agent-plugin-core/python/hook_runner.py:77-94

Event Recording and Tool Error Adaptation

Agent turns and tool executions are captured through specialized record handlers. The function default_record_stop() logs the assistant response without transcript parsing by retrieving the session repository via store.repo_for_session(), redacting sensitive substrings from last_assistant_message, and recording the clean text.

def default_record_stop(store: EvidenceStore, hook_input: dict):
    """Record the assistant's response without transcript parsing."""
    session_id = _session_id(hook_input)
    repo = store.repo_for_session(session_id, hook_input.get("cwd"))
    message = redact(hook_input.get("last_assistant_message", "")).strip()
    if message:
        store.record_assistant_response(repo, session_id, message)
    return repo, session_id
 

Sources: integrations/agent-plugin-core/python/hook_runner.py:52-60

Specific integrations like the Codex plugin adapter augment default behavior by handling subagent lifecycle events and normalizing tool execution responses. Specifically, _post_tool() inspects tool response objects for numeric exit codes or boolean flags (isError, success) to reliably flag tool failures before passing payloads to record_tool().

response = payload.get("tool_response")
failed = None
if isinstance(response, dict):
    exit_code = response.get("exit_code")
    if isinstance(exit_code, int) and not isinstance(exit_code, bool):
        failed = exit_code != 0
    elif isinstance(response.get("isError"), bool):
        failed = response["isError"]
    elif isinstance(response.get("success"), bool):
        failed = not response["success"]
if failed:
    payload = {**payload, "error": response}
record_tool(store, payload, failed=failed)

Sources: integrations/codex-plugin/hooks/adapter.py:43-55

Warning

Boolean evaluation order for tool responses checks non-boolean integers first (exit_code != 0), followed by isError, and finally success. Ensure tool response schemas populate these keys accurately to prevent misclassified execution states.

Sources: integrations/codex-plugin/hooks/adapter.py:45-52

Detached Background Flush Worker Lifecycle

Subprocess Handoff and Payload Initialization

When print-mode host processes or exit hooks face termination constraints (such as Claude Code canceling SessionEnd hooks as parent streams close), hooks persist their input payloads and launch flush_worker.py as a detached background process in a new session.

def main() -> int:
    if len(sys.argv) != 2:
        return 2
    handoff_path = Path(sys.argv[1])
    os.environ["MEM0_CODE_HANDOFF_PATH"] = str(handoff_path)
    harness = os.environ.get("MEM0_PLUGIN_HARNESS")
    if harness:
        source_tag = os.environ.get("MEM0_PLUGIN_SOURCE_TAG", "")
        configure_harness(
            harness,
            env_prefix=os.environ.get("MEM0_PLUGIN_ENV_PREFIX", ""),
            data_dir_name=os.environ.get("MEM0_PLUGIN_DATA_DIR_NAME", ""),
            source_tag=source_tag,
        )
        telemetry.init(harness=harness, source_tag=source_tag.upper())
    completed = False

Sources: integrations/agent-plugin-core/python/flush_worker.py:25-40, integrations/antigravity-plugin/core/flush_worker.py:25-40, integrations/cursor-plugin/core/flush_worker.py:25-40, integrations/kimi-plugin/core/flush_worker.py:25-40

The worker expects exactly one command-line argument: the file path to the handoff JSON payload (sys.argv[1]). It assigns this path to the environment variable MEM0_CODE_HANDOFF_PATH. If MEM0_PLUGIN_HARNESS is configured in the environment, the worker initializes the plugin harness configuration and sets up telemetry using telemetry.init().

try:
    payload = json.loads(handoff_path.read_text(encoding="utf-8"))
    delay = float(payload.get("delay_seconds") or 0)
    if delay > 0:
        payload.pop("delay_seconds", None)
        temporary = handoff_path.with_suffix(f".{os.getpid()}.tmp")
        try:
            temporary.write_text(json.dumps(payload), encoding="utf-8")
            temporary.replace(handoff_path)
        finally:
            temporary.unlink(missing_ok=True)
        time.sleep(delay)
        if not handoff_path.exists():
            return 0

Sources: integrations/agent-plugin-core/python/flush_worker.py:41-54, integrations/antigravity-plugin/core/flush_worker.py:41-54, integrations/cursor-plugin/core/flush_worker.py:41-54, integrations/kimi-plugin/core/flush_worker.py:41-54

After loading the payload, the worker inspects delay_seconds. If a positive delay is specified, the worker pops the delay key, writes the modified payload to a temporary file (.<pid>.tmp), atomically replaces the original handoff file, and sleeps for the requested duration.

hook_input = payload.get("hook_input") or {}
reason = str(payload.get("reason") or "checkpoint")
wait_for_inflight = bool(payload.get("wait_for_inflight"))
store = EvidenceStore()
try:
    if wait_for_inflight:
        session_id = str(
            hook_input.get("session_id") or "unknown-session"
        )
        repo = store.repo_for_session(session_id, hook_input.get("cwd"))
        deadline = time.monotonic() + float(
            os.environ.get("MEM0_CODE_EXTRACTION_WAIT_SECONDS", "120")
        )
        while (
            store.has_inflight_flush(repo.identity, session_id)
            and time.monotonic() < deadline
        ):
            touch_handoff_heartbeat()
            time.sleep(0.25)

Sources: integrations/agent-plugin-core/python/flush_worker.py:55-73, integrations/antigravity-plugin/core/flush_worker.py:55-73, integrations/cursor-plugin/core/flush_worker.py:55-73, integrations/kimi-plugin/core/flush_worker.py:55-73

Heartbeat Tracking and Inflight Checkpointing

If the payload configures wait_for_inflight as true, the worker monitors existing database activity before executing the final checkpoint. It retrieves the active repository context for the session and polls store.has_inflight_flush() until active flushes clear or a timeout is reached.

deadline = time.monotonic() + float(
    os.environ.get("MEM0_CODE_EXTRACTION_WAIT_SECONDS", "120")
)
while (
    store.has_inflight_flush(repo.identity, session_id)
    and time.monotonic() < deadline
):
    touch_handoff_heartbeat()
    time.sleep(0.25)

Sources: integrations/agent-plugin-core/python/flush_worker.py:65-73, integrations/antigravity-plugin/core/flush_worker.py:65-73, integrations/cursor-plugin/core/flush_worker.py:65-73, integrations/kimi-plugin/core/flush_worker.py:65-73

Note

The extraction wait timeout defaults to 120 seconds, configurable via the MEM0_CODE_EXTRACTION_WAIT_SECONDS environment variable. During each 0.25-second poll iteration, the worker invokes touch_handoff_heartbeat().

Sources: integrations/agent-plugin-core/python/flush_worker.py:66-73, integrations/agent-plugin-core/python/memory_core.py:1893-1901

The helper touch_handoff_heartbeat() updates the modification timestamp of the handoff file via os.utime(path), signaling to external supervisor processes that the background worker remains active.

def touch_handoff_heartbeat() -> None:
    """Mark the worker's handoff file alive so recovery does not relaunch it."""
    path = os.environ.get("MEM0_CODE_HANDOFF_PATH", "")
    if not path:
        return
    try:
        os.utime(path)
    except OSError:
        pass

Sources: integrations/agent-plugin-core/python/memory_core.py:1893-1901

Status Evaluation and Recovery Cleanup

Once inflight checks conclude, the worker invokes checkpoint_session(store, hook_input, reason) to process and transmit the session evidence packet to the platform backend.

    # Hooks capture the conversation before handoff; the worker only flushes it.
    result = checkpoint_session(store, hook_input, reason)
    print(json.dumps(result, sort_keys=True), flush=True)
    completed = result.get("status") in {
        "semantic-succeeded",
        "explicitly-stored",
        "nothing-to-flush",
    }
finally:
    store.close()
return 0

Sources: integrations/agent-plugin-core/python/flush_worker.py:74-84, integrations/antigravity-plugin/core/flush_worker.py:74-84, integrations/cursor-plugin/core/flush_worker.py:74-84, integrations/kimi-plugin/core/flush_worker.py:74-84

The completion status determines how the handoff file is cleaned up in the finally block. Successful status codes validate completion, unlinking the handoff file. If incomplete and the file uses a .running suffix, it is renamed back to .json for recovery.

finally:
    telemetry.flush()
    if completed:
        try:
            handoff_path.unlink()
        except OSError:
            pass
    elif handoff_path.suffix == ".running":
        try:
            handoff_path.replace(handoff_path.with_suffix(".json"))
        except OSError:
            pass
 

Sources: integrations/agent-plugin-core/python/flush_worker.py:85-97, integrations/antigravity-plugin/core/flush_worker.py:85-97, integrations/cursor-plugin/core/flush_worker.py:85-97, integrations/kimi-plugin/core/flush_worker.py:85-97

Status CodeActionOutcome
semantic-succeededComplete executionHandoff file is unlinked
explicitly-storedComplete executionHandoff file is unlinked
nothing-to-flushComplete executionHandoff file is unlinked
Other / ErrorIncomplete execution.running suffix renamed to .json

Sources: integrations/agent-plugin-core/python/flush_worker.py:77-81, integrations/agent-plugin-core/python/flush_worker.py:87-96

MCP Server Memory Search Tool

JSON-RPC Protocol Handling and Request Routing

The Model Context Protocol (MCP) server runs over standard input and output streams, processing newline-delimited JSON-RPC 2.0 messages via main(). Incoming payloads are parsed and routed through handle_request(), which supports methods including initialize, ping, tools/list, and tools/call. Initialization responds with protocol version 2024-11-05, tool capabilities, and server metadata containing name mem0 and PLUGIN_VERSION.

def handle_request(message: Any) -> dict[str, Any] | None:
    if not isinstance(message, dict):
        return None
    request_id = message.get("id")
    method = message.get("method")
 
    if method == "notifications/initialized":
        return None
    if method == "initialize":
        requested = (message.get("params") or {}).get("protocolVersion")
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "protocolVersion": requested or PROTOCOL_VERSION,
                "capabilities": {"tools": {"listChanged": False}},
                "serverInfo": {"name": "mem0", "version": PLUGIN_VERSION},
            },
        }
    if method == "ping":
        return {"jsonrpc": "2.0", "id": request_id, "result": {}}
    if method == "tools/list":
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "tools": [
                    {
                        "name": TOOL_NAME,
                        "description": TOOL_DESCRIPTION,
                        "inputSchema": TOOL_SCHEMA,
                        "annotations": {
                            "readOnlyHint": True,
                            "idempotentHint": True,
                            "openWorldHint": True,
                        },
                    }
                ]
            },
        }

Sources: integrations/agent-plugin-core/python/mcp_server.py:152-191

Note

Unhandled methods return a JSON-RPC error with code -32601 ("Method not found"), while parse errors yield code -32700 and internal exceptions return code -32603. Notifications such as notifications/initialized return None and produce no output.

Sources: integrations/agent-plugin-core/python/mcp_server.py:158-159, integrations/agent-plugin-core/python/mcp_server.py:206-231

Tool Schema and Argument Validation

When a client invokes tools/call for search_memories, arguments are validated by _validate_arguments() against a strict input schema. The tool accepts a required query string alongside optional parameters top_k, category, scope, and run_id.

ParameterTypeValidation RulesDescription
querystringNon-empty, max 2000 charsDirect question about earlier work in the repository
top_kintegerInteger between 1 and 20 (inclusive)Maximum memories to return
categorystringMust be in CODING_MEMORY_CATEGORY_NAMESOptional memory category filter
scopestringMust be in SEARCH_SCOPESSearch boundary (repo, dir, or mine)
run_idstringNon-empty string when providedCoding-agent session ID restriction

Sources: integrations/agent-plugin-core/python/mcp_server.py:28-68, integrations/agent-plugin-core/python/mcp_server.py:75-112

Warning

Passing boolean values for integer parameters like top_k or supplying unknown keys outside the permitted set triggers a ToolInputError, which is caught and returned to the client as an MCP tool error response with isError: true.

Sources: integrations/agent-plugin-core/python/mcp_server.py:78-83, integrations/agent-plugin-core/python/mcp_server.py:93-96, integrations/agent-plugin-core/python/mcp_server.py:201-202

Workspace Resolution and Execution

The helper _workspace_cwd(params) inspects request metadata for x-codex-turn-metadata workspaces to determine the active working directory. If absent, call_search_memories() falls back to CLAUDE_PROJECT_DIR or the current working directory via resolve_repo(), then executes search_memories() with operation mcp-search.

def call_search_memories(arguments: Any, cwd: str | None = None) -> str:
    query, top_k, category, scope, run_id = _validate_arguments(arguments)
    repo = resolve_repo(cwd or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd())
    result = search_memories(
        None,
        repo,
        None,
        query,
        top_k=top_k,
        category=category,
        scope=scope,
        run_id=run_id,
        operation="mcp-search",
    )
    return format_search_result(result)
 
 
def _workspace_cwd(params: dict[str, Any]) -> str | None:
    meta = params.get("_meta")
    if not isinstance(meta, dict):
        return None
    metadata = meta.get("x-codex-turn-metadata")
    if not isinstance(metadata, dict):
        return None
    workspaces = metadata.get("workspaces") or {}
    if isinstance(workspaces, dict):
        return next((path for path in workspaces if isinstance(path, str) and path), None)
    return None

Sources: integrations/agent-plugin-core/python/mcp_server.py:115-142

Telemetry Spooling and Identity Management

Spooling Architecture and Non-Blocking Recording

Telemetry events record session details locally without blocking tool execution or touching the network during short-lived agent turns. The record function appends a single JSON line to a local spool file within a strict 3 to 6-second hook execution budget.

Note

Telemetry never transmits sensitive content such as prompts, memory text, queries, file paths, repository names, or raw API keys. Recorded payloads are restricted to event names, durations, coarse outcomes, counts, and identifiers hashed with a random per-installation salt.

Sources: integrations/agent-plugin-core/python/telemetry.py:8-17

Installation State and Salt Management

The installation salt is generated once and memoized using an atomic file creation mechanism (O_CREAT |O_EXCL) located at telemetry-salt within the data directory. This design isolates salt generation from identity dictionaries, preventing concurrent processes from overwriting repository hashes or splitting user identification during rapid tool invocations. Telemetry can be completely disabled across any process by setting the environment variable MEM0_TELEMETRY to false, 0, no, or off.

Sources: integrations/agent-plugin-core/python/telemetry.py:115-149

Configuration Constants and Claim Parameters

The telemetry module defines several operational constants governing spool limits, batch sizes, timeout boundaries, and claim retry mechanics for background delivery workers.

Constant NameValuePurpose
SPOOL_LIMIT_BYTES262144 (256 KB)Maximum size threshold for the local telemetry spool file
BATCH_SIZE100Maximum number of events sent in a single PostHog batch request
SEND_TIMEOUT5Maximum network timeout duration in seconds for HTTP requests
CLAIM_STALE_SECONDS120Time window before an unacknowledged claim is considered stale
CLAIM_EXPIRY_SECONDS604800 (7 days)Total expiration period for parked claims
MAX_CLAIM_ATTEMPTS3Maximum retry attempts before a batch claim is permanently discarded
MAX_PARKED_PER_RUN3Bound on parked claims drained per flush run after the live spool
RETRY_COOLDOWN_SECONDS60Cooldown period added per prior attempt before a released claim is reclaimable

Sources: integrations/agent-plugin-core/python/telemetry.py:98-112

Command-Line Diagnostics and User Controls

Status Verification and Health Diagnostics

The CLI tooling exposed by memory_cli.py provides administrative commands for inspecting plugin state, verifying environment configuration, and running diagnostic checks. Invoking status retrieves summary metrics including pause state, repository identity, local data path, API key configuration status, total recorded session details, memory updates, returned memories, and subagent runs. When executed with --json, these properties are serialized directly as a JSON object, while default execution formats human-readable metrics through _print_status. The doctor command executes diagnostic validations against the current working directory, printing individual check outcomes as PASS or FAIL with detailed error descriptions, returning an exit code of 0 on success or 1 if any check fails.

def _print_status(value: dict) -> None:
    last = value.get("last_operation") or {}
    print(f"Mem0: {'paused' if value['paused'] else 'active'}")
    print(f"Repository: {value['repo_id']}")
    print(f"Local data: {value['data_dir']}")
    print(f"API key: {'configured' if value['api_key_configured'] else 'missing'}")
    print(
        "Saved on this computer: "
        f"{value['events']} session details, {value['flushes']} memory updates"
    )
    print(
        f"Used in this repository: {value['retrievals']} memories returned, "
        f"{value['subagent_runs']} subagent runs"
    )
    if last:
        item_label = ""
        if last["operation"] in {"flush", "flush-retry"}:
            item_label = f", {last['item_count']} memories"
        operation = (
            "memory update"
            if last["operation"] in {"flush", "flush-retry"}
            else last["operation"].replace("-", " ")
        )
        print(
            f"Last {operation}: "
            f"{'succeeded' if last['success'] else 'failed'} "
            f"({last['duration_ms']:.1f} ms{item_label})"
        )
    subagent = value.get("last_subagent") or {}
    if subagent:
        state = "finished" if subagent.get("stopped_at") else "started"
        print(
            "Last subagent: "
            f"{state}, received {subagent['context_chars']} characters of memory, "
            f"agent {subagent['agent_id']}"
        )
 

Sources: integrations/agent-plugin-core/python/memory_cli.py:23-59, integrations/agent-plugin-core/python/memory_cli.py:67-72, integrations/agent-plugin-core/python/memory_cli.py:92-116

Configuration Management and Repository Forgetting

Users can toggle memory persistence dynamically using the pause and resume commands, which update the paused setting in the evidence store to true or false respectively. Repository data removal is handled by the forget command, which requires explicit confirmation via the --yes flag to prevent accidental data destruction. If --yes is omitted, the CLI aborts with exit code 2 and displays a warning message. When confirmed, forget invokes forget_remote_repo() if --include-project-memory and --remote are specified, alongside store.forget_local_repo() to clear local SQLite evidence for the repository identity.

Caution

The forget command permanently destroys local session evidence and, when --remote is supplied, deletes scoped repository and user memories from the Mem0 platform. This operation requires the --yes safety flag and cannot be undone.

Sources: integrations/agent-plugin-core/python/memory_cli.py:76-80, integrations/agent-plugin-core/python/memory_cli.py:117-146

Was this page helpful?