StixDB Engine API Skill

SkillDocs & knowledge

Use this skill whenever the user asks about using StixDBEngine directly for memory management, agentic context, reasoning, chat, streaming, or advanced memory operations. Covers engine.store(), engine.ask(), engine.chat(), engine.stream_chat(), engine.retrieve(), engine.ingest_file(), engine.recursive_chat(), engine.stream_recursive_chat(), and all other engine-level operations. Use when the user is working with the Python library (not the CLI or raw REST calls).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the StixDB Engine API Skill skill

What this skill tells your AI

The instructions your AI receives, as published by pr0fe5s0r/stixdb in skills/API/SKILL.md and read by ahel’s review.

StixDBEngine is a REST client that connects to a running StixDB server and exposes a clean async Python API for all StixDB operations. It does not run anything in-process — all work is delegated to the server over HTTP.

Prerequisite: start the server first with stixdb daemon start (or stixdb serve).


Quick Start

from stixdb import StixDBEngine, StixDBConfig

# Connect with defaults (localhost:4020, no auth)
engine = StixDBEngine()
await engine.store("my_agent", content="Alice leads the payments team.")
results = await engine.retrieve("my_agent", "payments lead", top_k=5)
response = await engine.ask("my_agent", "Who leads payments?")
print(response.answer)   # Markdown string with citations

# or use async context manager (recommended — ensures cleanup)
async with StixDBEngine() as engine:
    response = await engine.ask("my_agent", "Who leads payments?")
    print(response.answer)

# With explicit config
config = StixDBConfig(url="http://localhost:4020", api_key="your-key")
async with StixDBEngine(config=config) as engine:
    ...

# From environment variables
config = StixDBConfig.from_env()
async with StixDBEngine(config=config) as engine:
    ...

Configuration

StixDBConfig holds only connection settings. The server manages all storage, LLM, and embedding config.

from stixdb import StixDBConfig

config = StixDBConfig(
    url="http://localhost:4020",   # server base URL (default: http://localhost:4020)
    api_key="your-secret-key",     # must match STIXDB_API_KEY on server (optional)
    timeout=120.0,                 # HTTP timeout in seconds for non-streaming requests
)

From environment (recommended):

config = StixDBConfig.from_env()

Environment variables:

STIXDB_URL=http://localhost:4020   # server URL
STIXDB_API_KEY=your-secret-key     # optional — must match server's STIXDB_API_KEY
STIXDB_TIMEOUT=120                 # HTTP timeout seconds (default 120)

Inline construction (no config object needed):

engine = StixDBEngine(url="http://prod-server:4020", api_key="sk-...")

Storing Memories

engine.store()

result = await engine.store(
    collection="agent_name",      # required — collection name
    content="The memory text.",   # required

    # Classification
    node_type="fact",             # fact | experience | goal | rule | pattern
    tier="episodic",              # episodic | semantic | procedural | working

    # Importance and lifecycle
    importance=0.7,               # 0.0 (ephemeral) → 1.0 (critical)
    pinned=False,                 # True = never pruned by decay

    # Metadata
    source="module_name",
    source_agent_id="worker_1",
    tags=["tag1", "tag2"],
    metadata={"custom": "value"},
    node_id="optional_custom_id", # auto-generated if omitted
)
# Returns: {"id": "uuid", "collection": "...", "status": "stored", ...}

Importance guide:

ScoreUse for
0.95In-progress state, user preferences, critical decisions
0.9Bug fixes, patterns, architecture rules
0.85Module maps, API surfaces, session summaries
0.7Normal facts (default)
0.5Background / historical context

engine.bulk_store()

items = [
    {"content": "Fact 1", "tier": "semantic", "importance": 0.8, "tags": ["a"]},
    {"content": "Fact 2", "tier": "episodic", "importance": 0.5},
]
result = await engine.bulk_store(collection="agent_name", items=items)

Each item accepts the same fields as store(). Use for batch imports and initialization.


Ingesting Files

engine.ingest_file()

result = await engine.ingest_file(
    collection="agent_name",
    filepath="/path/to/document.pdf",
    tags=["docs", "v1"],
    chunk_size=600,       # characters per chunk (default 600)
    chunk_overlap=150,    # overlap between chunks (default 150)
)
# Returns: {"source_name": "...", "node_ids": [...], "ingested_chunks": N}

Supported types: .pdf, .md, .rst, .html, .txt, .csv, .json, .jsonl, .yaml, .toml, .py, .js, .ts, .go, .rs, .sql, and more. Binary files are skipped automatically.

Deduplication is built in — re-ingesting the same file doesn't create duplicate chunks.

engine.ingest_folder()

result = await engine.ingest_folder(
    collection="agent_name",
    folderpath="/path/to/docs",
    tags=["documentation"],
    chunk_size=600,
    chunk_overlap=150,
    recursive=True,
)
# Returns: {"files_processed": N, "files_skipped": N, "ingested": [...], "skipped": [...]}

Respects .gitignore. Skips node_modules, .git, and binary files automatically.

Chunk size guidance:

  • Technical docs / code: 500–800
  • Prose / narrative: 1000–1500
  • Always keep overlap at ~20% of chunk size

Retrieval

engine.retrieve() — raw retrieval, no LLM

nodes = await engine.retrieve(
    collection="agent_name",
    query="user preferences",
    top_k=10,
    threshold=0.1,        # minimum combined score (default 0.1)
    depth=1,              # graph expansion hops (default 1)
    mode="hybrid",        # "hybrid" (default) | "keyword" | "semantic"
)
# Returns: list of dicts, each with node fields + "score"

Retrieval modes:

ModeHow it worksWhen to use
"hybrid" (default)0.7 × semantic_score + 0.3 × keyword_scoreBest general recall
"keyword"Tag overlap + content term match. No embedding API call. ~5msFast exact-term lookups
"semantic"Vector embedding + cosine similarityParaphrase / conceptual queries

Use retrieve() when you want ranked nodes without LLM cost. Use ask() when you need synthesis.


Asking Questions (LLM Reasoning)

engine.ask() — single-pass reasoning

response = await engine.ask(
    collection="agent_name",
    question="What are the user's accessibility needs?",

    # Retrieval tuning
    top_k=15,
    threshold=0.2,
    depth=2,

    # LLM control
    thinking_steps=1,       # 1 = single-pass; 2+ = multi-hop reasoning
    hops_per_step=4,        # retrieval hops per thinking step
    system_prompt=None,     # override system instructions
    output_schema=None,     # enforce a JSON schema on the answer field
    max_tokens=None,        # cap LLM output; None = server default
)

ContextResponse fields:

response.answer           # Markdown string — headers, bullets, inline citations [1][2], Sources section
response.reasoning_trace  # Internal chain-of-thought (not shown to user)
response.sources          # list[dict] — nodes used to generate the answer
response.confidence       # float 0–1, self-reported by the LLM
response.model_used       # e.g. "gpt-4o"
response.latency_ms       # end-to-end server latency in milliseconds
response.is_complete      # False when the LLM indicated it needs more information
response.suggested_query  # Optional follow-up search query suggested by the LLM

# Iterate sources
for src in response.sources:
    print(src["content"], src.get("relevance"))

Answer format: ask() returns rich Markdown with inline citations [1], [2] matching numbered sources, and a Sources section at the end. Pass response.answer directly to a Markdown renderer.

Multi-hop reasoning (thinking_steps > 1)

response = await engine.ask(
    collection="agent_name",
    question="Summarise all known bugs and their root causes.",
    thinking_steps=3,     # 3 retrieval-reasoning cycles
    hops_per_step=4,      # up to 4 graph hops per cycle
    top_k=25,
    depth=3,
)

At each step the LLM decides what to search next. Higher thinking_steps = deeper answers at the cost of more LLM calls.


Chat (Conversational, with Session History)

engine.chat() — single turn with session memory

response = await engine.chat(
    collection="agent_name",
    question="Help me understand the auth flow.",
    session_id="conv_123",    # optional — enables multi-turn history
    top_k=15,
    depth=2,
    temperature=None,
    max_tokens=None,
)
# Returns: ContextResponse (same as ask())
print(response.answer)   # Markdown

session_id groups messages into a conversation. The history is passed to the LLM for context on each turn.

engine.recursive_chat() — multi-hop single question

response = await engine.recursive_chat(
    collection="agent_name",
    question="What are the key revenue drivers and which accounts are at risk?",
    session_id="conv_123",    # optional
    thinking_steps=2,         # autonomous retrieval cycles
    hops_per_step=4,
    threshold=0.7,            # confidence threshold to stop early
    temperature=None,
    max_tokens=None,
)
# Returns: ContextResponse
# response.reasoning_trace includes full thinking chain

Use when you want the engine to autonomously refine its retrieval across multiple hops before synthesising a final answer.


Streaming

engine.stream_chat() — stream tokens from a single-turn chat

async for chunk in engine.stream_chat(
    collection="agent_name",
    question="Explain the auth flow.",
    session_id="conv_123",
    top_k=15,
    depth=2,
    temperature=None,
    max_tokens=None,
):
    if chunk.get("type") == "node_count":
        print(f"Retrieved {chunk['count']} nodes")
    elif chunk.get("type") == "answer":
        print(chunk["content"], end="", flush=True)

Chunk format: each chunk is a dict:

typecontent / extra fieldsWhen emitted
"node_count"count: intOnce, before first token
"answer"content: str — token(s)Repeatedly as the LLM generates

The stream ends when the async iterator is exhausted (no sentinel needed from the caller side).

engine.stream_recursive_chat() — stream tokens from multi-hop reasoning

async for chunk in engine.stream_recursive_chat(
    collection="agent_name",
    question="Deep dive on the storage architecture.",
    session_id=None,
    thinking_steps=2,
    hops_per_step=4,
    temperature=None,
    max_tokens=None,
):
    if chunk.get("type") == "thinking":
        print(f"[thinking] {chunk['content']}")
    elif chunk.get("type") == "answer":
        print(chunk["content"], end="", flush=True)

Additional chunk type for recursive streaming:

typeMeaning
"thinking"Narration emitted before each hop
"node_count"Nodes retrieved in this hop
"answer"Final answer token

Memory Tiers

TierPurposeExample
episodicSpecific events, recent interactions"User clicked dark mode at 15:30Z"
semanticStable facts, learned preferences"User prefers dark mode"
proceduralHow-to steps, workflows"Reset password: click forgot → check email → follow link"
workingAuto-promoted hot facts (managed by the agent)Summary nodes created during consolidation

Node Types

TypeUse for
factStatic assertions — "The office is on 5th Street"
experienceEvents with timestamp — "User visited at 15:30Z"
goalObjectives — "User wants fewer notifications"
ruleConditional logic — "If offline, queue messages"
patternRecurring behaviours — "User logs in at 9am daily"

Graph Operations

# Add an explicit edge between two nodes
await engine.add_relation(
    collection="agent_name",
    from_node="node_uuid_a",
    to_node="node_uuid_b",
    relation_type="supports",   # influences | contradicts | supports | related_to | caused_by
    metadata={"strength": 0.8},
)

# Graph statistics
stats = await engine.get_graph_stats(collection="agent_name")
# {"node_count": 312, "edge_count": 87, ...}

# Collection statistics (tier breakdown)
stats = await engine.get_collection_stats(collection="agent_name")

# Remove duplicate nodes
result = await engine.dedupe_collection(collection="agent_name", dry_run=False)

Collection Management

# Collections are created lazily on first store/ask
await engine.store("new_collection", content="First memory.")

# List all collections
collections = engine.list_collections()                  # sync
collections = await engine.list_collections_async()      # async

# Unload from memory (data persists on disk)
await engine.drop_collection(collection="agent_name")

# Permanently delete all data (irreversible)
result = await engine.delete_collection(collection="agent_name")
# {"deleted_nodes": N, "deleted_clusters": N}

Background Agent

# Manually trigger a maintenance cycle (consolidation, decay, pruning)
result = await engine.trigger_agent_cycle(collection="agent_name")
# {"cycle_number": N, "nodes_processed": N, ...}

# Check agent state
status = await engine.get_agent_status(collection="agent_name")
# {"state": "idle" | "processing", "last_cycle_timestamp": "..."}

What a cycle does:

  1. Merges nodes above 0.88 cosine similarity into a summary node
  2. Collapses exact duplicates (highest importance wins)
  3. Decays archived nodes with a 48-hour half-life
  4. Prunes nodes below 0.05 importance

The cycle runs automatically in the background every 30 seconds (configurable via STIXDB_AGENT_CYCLE_INTERVAL on the server).


Observability

traces = engine.get_traces(collection="agent_name", limit=10)
for trace in traces:
    print(f"{trace['timestamp']}: {trace['message']}")

Error Handling

try:
    response = await engine.ask(collection="agent", question="...?")
except httpx.ConnectError:
    print("Server not running — start with: stixdb daemon start")
except httpx.HTTPStatusError as e:
    print(f"Server error {e.response.status_code}: {e.response.text}")

Common causes:

ErrorCauseFix
ConnectErrorServer not runningstixdb daemon start
401 UnauthorizedWrong or missing API keySet STIXDB_API_KEY to match server
404 Not FoundWrong collection name or URLCheck stixdb collections list
empty answertop_k too low or question too genericIncrease top_k, be more specific

API Reference

MethodReturnsPurpose
engine.store(collection, content, ...)dictStore a single memory node
engine.bulk_store(collection, items)dictStore many nodes efficiently
engine.ingest_file(collection, filepath, ...)dictParse and chunk a file
engine.ingest_folder(collection, folderpath, ...)dictProcess a directory recursively
engine.retrieve(collection, query, mode="hybrid", ...)list[dict]Raw retrieval without LLM
engine.ask(collection, question, thinking_steps=1, ...)ContextResponseLLM-synthesised Markdown answer with citations
engine.chat(collection, question, session_id=None, ...)ContextResponseSingle-turn chat with session history
engine.stream_chat(collection, question, ...)AsyncIterator[dict]Stream answer tokens chunk-by-chunk
engine.recursive_chat(collection, question, thinking_steps=2, ...)ContextResponseMulti-hop autonomous reasoning
engine.stream_recursive_chat(collection, question, ...)AsyncIterator[dict]Stream multi-hop reasoning with thinking narration
engine.add_relation(collection, from_node, to_node, ...)dictCreate an explicit graph edge
engine.get_graph_stats(collection)dictNode/edge counts
engine.get_collection_stats(collection)dictTier breakdown
engine.dedupe_collection(collection, dry_run)dictRemove duplicate nodes
engine.trigger_agent_cycle(collection)dictRun consolidation/decay cycle manually
engine.get_agent_status(collection)dictBackground agent state
engine.drop_collection(collection)Unload from memory (data kept)
engine.delete_collection(collection)dictPermanently delete all data
engine.list_collections()list[str]All known collections (sync)
engine.list_collections_async()list[str]All known collections (async)
engine.get_traces(collection, limit)list[dict]Recent operation traces

Patterns

Pattern 1 — Knowledge base with Q&A

from stixdb import StixDBEngine

async with StixDBEngine() as engine:
    await engine.ingest_folder("kb", folderpath="./docs", tags=["official"])

    response = await engine.ask(
        "kb",
        "How do I reset my password?",
        top_k=20,
        depth=2,
    )
    print(response.answer)   # Markdown with citations

Pattern 2 — Streaming chat to a UI

from stixdb import StixDBEngine, StixDBConfig

config = StixDBConfig.from_env()

async def stream_to_client(collection: str, question: str):
    async with StixDBEngine(config=config) as engine:
        async for chunk in engine.stream_chat(collection, question=question):
            if chunk.get("type") == "answer":
                yield chunk["content"]   # send token to WebSocket / SSE

Pattern 3 — Multi-agent shared context

from stixdb import StixDBEngine

async with StixDBEngine() as engine:
    # Agent 1 stores its findings
    await engine.store("shared", content="Found bug in payment processor", tags=["bugs"], source="agent_1")

    # Agent 2 stores its findings
    await engine.store("shared", content="Root cause: timeout on retry", tags=["bugs"], source="agent_2")

    # Any agent synthesises across all findings
    response = await engine.ask("shared", "What bugs were found and what caused them?", top_k=20)
    print(response.answer)

Pattern 4 — Deep reasoning with multi-hop

from stixdb import StixDBEngine

async with StixDBEngine() as engine:
    response = await engine.recursive_chat(
        "proj_myapp",
        question="Summarise all architecture decisions and their consequences.",
        thinking_steps=3,
        hops_per_step=4,
    )
    print(response.answer)
    print(response.reasoning_trace)   # full thinking chain

Pattern 5 — Connect to remote server

from stixdb import StixDBEngine, StixDBConfig

config = StixDBConfig(
    url="https://stixdb.internal.mycompany.com",
    api_key="prod-secret-key",
    timeout=60.0,
)
async with StixDBEngine(config=config) as engine:
    response = await engine.ask("prod_agents", "What is the current deployment status?")
    print(response.answer)

Signals

GitHub stars
74
Forks
10
Last commit
Apr 2026
Advanced
Catalog kind
skill
Gateway key
engine-memory-layer
Source
github.com/pr0fe5s0r/stixdb