Skip to content

Tools and facts

What an agent can do with the store. Five tools, deliberately — every tool description costs context in every session, so the list stays short.

The five tools

Tool Purpose
memory_search Hybrid BM25 + vector search over curated facts
memory_write Record a durable fact
memory_update Amend a fact; a text change supersedes rather than overwrites
memory_forget Retire a fact — soft, never destroyed
memory_recent Recently written or updated facts

Nothing is ever hard-deleted. memory_forget marks a fact retired and memory_update supersedes it under a new id, so the record of what was once believed survives the correction.

Fact kinds

Kind For
user Who you are: role, expertise, context
preference How you want work done
project Goals, constraints, or state of ongoing work
reference A pointer to something external: URL, ticket, dashboard
research Findings you gathered, with the conclusion stated
decision A choice made and the reasoning behind it

reference points at where something lives; research carries the finding itself.

The query log

Every search is recorded — the query, the best hit, and two counts: matched_count, how many facts the search found, and returned_count, how many you were handed once limit was applied. Both are needed: a returned_count of 2 means nothing on its own, because it reads the same whether the store held two facts or two hundred.

A search with matched_count: 0 is the interesting case. It is the only record of something you expected memory to know and it did not, and that evidence does not exist unless it is captured as it happens.

curl -H "Authorization: Bearer $ESKY_TOKEN" \
  http://192.168.1.10:8011/api/personal/queries?limit=20
{"profile": "personal",
 "queries": [{"query": "how do we deploy", "tags": [],
              "matched_count": 0, "returned_count": 0,
              "top_uid": null, "created_at": "2026-09-19T04:56:15+00:00"}]}

limit defaults to 50 and accepts 1–500; anything else is a 400, because a negative LIMIT means "no limit" to SQLite and would quietly defeat the cap. A row whose matched_count is unknown carries null rather than a guess.

The log is per profile and behind the same token as everything else, so the work log is not readable with the personal token. It is REST-only and not an MCP tool: it is for you reviewing the store, and every tool description costs context in every agent session.

Aggregates, for charting

Two endpoints summarise a profile over a window of days rather than listing rows. They exist because the listing above is capped at the 500 newest rows, so anything counted from it describes an unknown period rather than the one it claims.

curl -H "Authorization: Bearer $ESKY_TOKEN" \
  "http://192.168.1.10:8011/api/personal/queries/summary?days=30"
{"profile": "personal", "days": 30, "from": "2026-08-21", "to": "2026-09-19",
 "totals": {"searches": 142, "zero_match": 19, "unknown_matched": 0,
            "distinct_queries": 96},
 "daily": [{"date": "2026-08-21", "searches": 4, "zero_match": 1}],
 "match_buckets": [{"label": "0", "count": 19}],
 "top_queries": [{"query": "how do we deploy", "count": 3, "zero_match": 3}],
 "top_tags": [{"tag": "infra", "count": 11}],
 "top_facts": [{"uid": "Ax3f…", "count": 7, "title": "Deployment"}]}

zero_match counts searches that handed the caller nothing, which every row can answer. unknown_matched counts rows carrying no matched_count — reported separately rather than folded in, since counting them as misses would invent evidence that memory failed. daily covers every day in the window, including the quiet ones, so a chart drawn from it has no false gaps.

/api/{profile}/stats takes the same days and returns the store's shape alongside the counts it always returned: facts and retired, plus kinds, top_tags, daily (created and retired per day) and the oldest / newest timestamps it spans.

Both take days in 1–365, defaulting to 30; anything else is a 400.

The eskyClient web UI draws all of this at /metrics.php.


Next: operations — CLI, configuration, tokens · how isolation is enforced