Quickstart
In this guide you run Taisce on your machine, save one thing a user said, ask about it, and then forget it. There are seven steps. Each one shows the command, what you should see, and what just happened. You don't need an account anywhere, because the model runs on your own machine.
What you need
- Docker with Compose v2. Check with
docker compose version. The older standalonedocker-composewill not do. - Ollama, which runs the model that reads conversations. Check with
ollama --version. jq, to pull fields out of JSON, anduuidgen. On Linux withoutuuidgen, usecat /proc/sys/kernel/random/uuidwherever it appears.- An empty directory to work in. You do not need a checkout and you do not need Go: the compose
file names images a release published, for
linux/amd64andlinux/arm64, and pulls them.
1. Start the model
OLLAMA_HOST=0.0.0.0 ollama serve
Leave that running. In a second terminal:
ollama pull qwen3.6:35b-a3b-mxfp8
curl -s http://localhost:11434/v1/models | jq -r '.data[].id'
You should see the model you pulled:
qwen3.6:35b-a3b-mxfp8
Taisce uses this model to read each conversation turn and propose facts, and
OLLAMA_HOST=0.0.0.0 lets the containers reach it on your machine.
Which model?
Taisce asks the model for strict JSON built from a fixed list of relations, and not every model can keep to that. These were measured against the same 19-message test corpus (extraction models, 2026-09-11):
| Model | Where it runs | Keeps to the format? | Set it up with |
|---|---|---|---|
qwen3.6:35b-a3b-mxfp8 | Ollama, on your machine | Yes, all 19 cases | the steps above (the default) |
Qwen/Qwen3.8-27B, thinking off | vLLM, on a rented GPU | Yes, all 19 cases | demo-qwen3.8 |
deepseek-flash | the provider's hosted API | Yes, all 19 cases | deepseek |
qwen3.8:27b-mxfp8, thinking on | Ollama, on your machine | No answer: every call ran past the two-minute limit | not recommended |
With a hosted model, what people tell your agent leaves your machine and goes to that provider. Taisce only sends text to hosts you have listed in the allowlist, so that is always your decision.
A model that is not in this table may still work. Check it with make test-inference before you
rely on it.
Didn't work? If curl cannot connect, Ollama is not running, or it is listening only on
localhost; restart it with OLLAMA_HOST=0.0.0.0. If the model answers but nothing ever forms, see
memory isn't forming.
To use a hosted model instead, see reaching a model.
2. Start Taisce
Fetch the compose file for the release you want, then bring it up:
curl -fsSLO https://raw.githubusercontent.com/ensera-ai/taisce/v0.3.1/compose.yaml
docker compose up -d
docker compose ps -a --format '{{.Service}}\t{{.State}}\t{{.Health}}'
curl -s localhost:8080/ready
The first run pulls two images — the service and its PostgreSQL substrate — so give it a minute on a cold cache. Once the health checks pass, you should see:
api running healthy
bootstrap exited
manage running healthy
postgres running healthy
worker running healthy
{"status":"ready"}
Then check that the worker found a model it is allowed to use:
docker compose logs --no-log-prefix worker | grep -c 'MEMORY WILL NOT FORM'
0
A one-off bootstrap set up PostgreSQL, created a project called default and printed its tokens.
Now the api answers on port 8080, and the worker turns stored turns into facts in the
background.
Didn't work? If port 8080 is taken, run TAISCE_PORT=18080 docker compose up -d and use that
port below. If the count is 1 or more, see
memory never forms.
If api or worker keeps restarting, see
the serving process exits at start.
3. Get your token
docker compose logs --no-log-prefix bootstrap | grep 'token:'
You should see two tokens:
operator token: tsk_…
token: tsk_…
Save the second one, the project token, and try it:
export TOKEN=$(docker compose logs --no-log-prefix bootstrap | awk '/^token:/ {print $2}')
curl -sS localhost:8080/v1/freshness -H "Authorization: Bearer $TOKEN"
{"scope":"default","stored":null,"formed":null,"parked":0}
The project token opens memory for the default project, and null means nothing has been written
yet. The operator token is for running the instance and is refused on memory calls.
Didn't work? A 401 means the token is wrong, and the usual cause is picking up the operator
token by mistake (every call answers 401).
Tokens are printed once, on the first start. If the line is gone, issue a new one with
docker compose exec manage /taisce credential issue app --project default.
4. Save a memory
export KEY=$(uuidgen)
curl -sS -X POST localhost:8080/v1/observations \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @- <<EOF
{"idempotency_key": "$KEY",
"data_subject_id": "alice",
"messages": [{"role": "user", "content": "I work at Ensera and I live in Dublin."}]}
EOF
You should see a receipt, with HTTP status 201:
{"id":"…","scope":"default","log_offset":0}
Taisce stored Alice's turn and answered straight away, before any model looked at it. log_offset
is the turn's place in the project's log, and data_subject_id says the words are Alice's. Run the
same command again and you get the same receipt: the idempotency_key makes it a retry, not a
second turn.
Didn't work? A 400 invalid_turn says what is wrong in its message, such as a key that is not
a UUID. A 409 idempotency_conflict means the key was already used for something else, so make a
new one with uuidgen (409 idempotency_conflict).
5. Wait for it to form
while :; do
F=$(curl -sS localhost:8080/v1/freshness -H "Authorization: Bearer $TOKEN"); echo "$F"
case "$F" in *'"formed":null'*) sleep 2 ;; *) break ;; esac
done
You should see a few lines while the model works, then formed catches up:
{"scope":"default","stored":0,"formed":null,"parked":0}
{"scope":"default","stored":0,"formed":null,"parked":0}
{"scope":"default","stored":0,"formed":0,"parked":0}
The worker had the model read Alice's message and kept only the facts whose words it could find in
it. stored is the last turn Taisce holds, and formed is the last turn it has turned into facts,
so your turn is in memory once formed reaches its log_offset
(stored and formed).
Didn't work? If formed stays null, see
formed stays null. If the loop ends with "parked":1,
the worker gave up on the turn after several tries; see
turns are parked.
6. Ask a question
curl -sS -X POST localhost:8080/v1/recalls \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"question": "Where do I work?", "data_subject_id": "alice"}' \
| jq '{anchors, facts: [.facts[] | {predicate, object, quote: .evidence.quote}]}'
You should see Alice's facts, each with the words it came from (trimmed; the statements come from the model, so wording can vary):
{
"anchors": [{"entity_id": "…", "name": "…", "type": "…", "matched": "speaker"}],
"facts": [
{"predicate": "works_at", "object": "Ensera", "quote": "I work at Ensera"},
{"predicate": "lives_in", "object": "Dublin", "quote": "…"}
]
}
Because you named Alice as the data_subject_id, "I" meant Alice, so recall started from her and
returned her facts, without calling a model. Without a subject, ask by name instead, as in "What do
we know about Ensera?".
Didn't work? An empty facts usually means the subject is spelled differently from the one you
saved (Alice is not alice) or formed has not reached your turn yet
(recall returns nothing for "I").
7. Forget Alice
curl -sS -X POST localhost:8080/v1/erasures \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"data_subject_id": "alice", "reason": "right to erasure"}' | jq
You should see a receipt (trimmed; which kinds appear depends on what formed):
{
"request_id": "…",
"scope": "default",
"data_subject_id": "alice",
"completed_at": "…",
"deleted": {"observation": 1, "fact": 2, "entity": …, "chunk": 1, …},
"residual": {"fact": 0, "entity": 0, "chunk": 0, …},
"clean": true
}
Ask again:
curl -sS -X POST localhost:8080/v1/recalls \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"question": "Where do I work?", "data_subject_id": "alice"}' | jq '.facts'
[]
Taisce deleted Alice's turn and everything built only from it. Then, in the same transaction, it
counted what still matched her: that count is residual, and clean is true because every count
is zero.
Didn't work? A 400 no_subject means the body named nobody, and an erasure never defaults to the
whole project. A receipt with clean: false names the kind of thing that survived; the
forget a person recipe explains how to read one, and
troubleshooting covers the rest of a first run.
When you are done
docker compose down stops everything. Your data stays in a Docker volume, so
docker compose up -d brings it back, and your token still works.
Next steps
- How it works: what happened in each step, in plain words.
- Examples: recipes for preferences, citations, forgetting, long conversations and coding agents.
- The HTTP API, by task: every call, option and error.
- Adapters for Python, Java and .NET, or MCP for coding agents: memory from the framework you already use.
- Troubleshooting: the problems a first run actually hits.