Engrava exposes a functional hook interface that lets external code observe and transform data flowing through the core pipeline:
| Interface | File | Semantic |
|---|---|---|
EngravaHooksProtocol |
domain/protocols/hooks.py |
Transformation — methods return modified data (custom scoring, MindQL extensions) |
For higher-level extension patterns (embedding providers, custom MindQL commands) see extensions.md.
- All four data-flow methods are
asyncand return a value. - Hooks must not raise — unexpected exceptions will propagate to the caller.
- Hooks must not have side effects that modify shared state; return an enriched copy instead.
- Engrava is
frozen=True-first — if you need to mutate aThoughtRecord, returnthought.model_copy(update={...}).
Only on_store and on_retrieve are invoked by the public engrava package
today. The remaining three are part of the protocol contract but are
reserved — core engrava does not call them (they exist for downstream
consumers and future use). Implement them if you want protocol conformance,
but do not expect core to invoke them.
| Method | When | Returns | Status in core |
|---|---|---|---|
on_store |
After a thought is persisted | ThoughtRecord (enriched or unchanged) |
active |
on_retrieve |
After a thought is loaded from storage | ThoughtRecord (enriched or unchanged) |
active |
score_function(thought, context) |
Custom relevance score | float |
reserved — not called by core |
decay_function(thought, elapsed_cycles) |
Per-thought decay factor | float in [0.0, 1.0] |
reserved — not called by core |
mindql_extension_registry() |
Register custom MindQL verbs | dict[str, MindQLExtension] |
reserved — core wires MindQL verbs via ExtensionManifest.mindql_extensions, not this hook |
from __future__ import annotations
from engrava.domain.protocols.hooks import DefaultEngravaHooks, ScoringContext
from engrava.domain.models.thought import ThoughtRecord
class RecencyBoostHooks(DefaultEngravaHooks):
"""Boosts score for recently updated thoughts."""
async def score_function(
self,
thought: ThoughtRecord,
context: ScoringContext,
) -> float:
# Add a small recency bonus for thoughts updated in a recent cycle.
# (updated_cycle is an int; updated_at is an ISO-8601 string, not a cycle.)
if context.current_cycle > 0:
age = context.current_cycle - thought.updated_cycle
return max(0.0, 1.0 - age / 100)
return 0.0
# Registration:
import aiosqlite
from engrava import SqliteEngravaCore
async def build_store(db_path: str) -> SqliteEngravaCore:
conn = await aiosqlite.connect(db_path)
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn, hooks=RecencyBoostHooks())
await store.ensure_schema()
return storeDefaultEngravaHooks is a no-op base class — override only the methods you
care about.
A custom MindQL command is an MindQLExtension whose handler is an async
callable. The executor invokes the handler with two positional arguments —
the open aiosqlite.Connection and the parsed extension-argument list — and
expects a list[dict[str, object]] back:
from __future__ import annotations
import aiosqlite
from engrava.domain.protocols.hooks import MindQLExtension
async def _recent_handler(
db: aiosqlite.Connection,
args: list[str], # noqa: ARG001 — this command takes no args
) -> list[dict[str, object]]:
"""Return the 100 most recently updated thoughts."""
cursor = await db.execute(
"SELECT thought_id, content FROM thought "
"ORDER BY updated_cycle DESC LIMIT 100"
)
rows = await cursor.fetchall()
return [{"thought_id": row["thought_id"], "content": row["content"]} for row in rows]
RECENT_COMMAND = MindQLExtension(
command_name="RECENT",
handler=_recent_handler,
description="Return the most recently updated thoughts.",
category="custom",
)The command is registered by listing it in an extension's
ExtensionManifest.mindql_extensions (the discovery path), or by passing it
through MindQLExtension-keyed extensions= when constructing the executor:
from engrava import MindQLExecutor, parse
executor = MindQLExecutor(conn, extensions={"RECENT": RECENT_COMMAND})
# parse() needs the registered verb names to recognise an extension command.
result = await executor.execute(parse("RECENT", known_extensions={"RECENT"}))Note:
EngravaHooksProtocol.mindql_extension_registry()is not consulted by core engrava (see §1.2) — declare custom verbs viaExtensionManifestor the executor'sextensions=argument as shown above.
Add a contract test to verify your implementation satisfies the protocol:
from engrava.domain.protocols.hooks import EngravaHooksProtocol
def test_my_hooks_satisfy_protocol() -> None:
assert isinstance(RecencyBoostHooks(), EngravaHooksProtocol)EngravaHooksProtocol is @runtime_checkable, so isinstance works without
meta-class magic.