Inside Kreo

Inside
Kreo

A picture book of how Kreo-Agent works today: every layer, every flow and every file.

Edition of 26 September 2026 · written for newcomers to the codebase · MTS

How to read this bookFront matter

How to read this book

Each spread has two pages. The left page tells you what is going on in plain words. The right page shows it as a picture. If you only look at the pictures, you will still understand most of Kreo.

The colour key used in every diagram
Blue: clientsThings you touch: the web page, the terminal, toasts.
Violet: Kreo's coreThe agent's own logic: API, orchestrator, scheduler, permissions.
Green: Python toolsExact, predictable code: tasks, weather, sunrise, search.
Orange: enginesThe AI model (llama-server) and the voice (Piper, Whisper).
Brown: storageSQLite database, config file, logs.
Getting around
← → arrow keys turn pages Swipe on a phone Contents opens the chapter list Scroll view shows everything at once, so Ctrl+F works
Kreo in numbers
73
Python files in kreo/, 8,133 lines
42
TypeScript files in frontend/src, 6,250 lines
236
test functions across 32 test files
1
local AI model: Qwen3-4B, about 2.5 GB
8765
the one port Kreo listens on
0
cloud AI calls. Everything thinks on your PC
ContentsFront matter

Contents

ContentsFront matter
Part I · The big pictureChapter 1
Chapter 1

Kreo in one picture

Kreo is a personal assistant that lives on your Windows PC. It is a Python program that keeps running in the background. It has its own AI model, its own voice and its own memory.

You talk to it through a client: the web page at http://127.0.0.1:8765, or the terminal command kreo chat. The client is only a window. The thinking, speaking, listening and remembering all happen inside the agent.

The AI model runs as a separate program, llama-server.exe, on port 8766. Kreo starts it, talks to it over HTTP and stops it again.

Kreo only goes on the internet when a question needs it: the weather, a web search, or finding your location if you allow that.

Remember this Local first. Close every browser tab and Kreo keeps working. Reminders still fire, toasts still appear, and it can still speak.
Kreo in one pictureChapter 1
Your Windows PC You Web clientReact · 127.0.0.1:8765 Terminalkreo chat · kreo status Windowstoasts · speakers · mic Kreo agent python -m kreo run FastAPI · /api · /ws Orchestrator Tools Scheduler SpeechPiper · faster-whisper llama-serverQwen3-4B · :8766 SQLitedata/kreo.db Config & logsconfig/ · logs/ The internet, only when a question needs it Open-Meteo weather · DuckDuckGo search · IP location (opt-in)
Everything inside the dashed line runs on your own machine. Blue arrows are HTTP and WebSocket calls from clients. Toasts and speech flow one way, from Kreo to you.

Two clients, one brain

The browser and kreo chat use the same WebSocket and get exactly the same answers.

A model you own

Qwen3-4B, a 4-billion-parameter model compressed to 4-bit (Q4_K_M), runs on the GTX 1650 and CPU.
Part I · The big pictureChapter 2
Chapter 2

Six layers, like a restaurant kitchen

The code is stacked in layers. Each layer only talks to the layer next to it. If you picture a small restaurant, every piece has an obvious job.

In the kitchenIn KreoFolder
Diners at the tableClients (web page, terminal)frontend/ kreo/cli/
The waiter who takes ordersAPI routes and the WebSocketkreo/api/
The head chef who decides every stepThe orchestratorkreo/core/agent/
A clever apprentice who only fills in order slipsThe AI modelkreo/llm/
The kitchen stationsTools: tasks, weather, searchkreo/core/tools/
The kitchen timerThe schedulerkreo/core/scheduler/
The ledger and the pantrySQLite, config, logskreo/database/ config/
The key idea The apprentice (the model) is talented with words but is never allowed to touch the stove. It fills in a form. The head chef (Python) reads the form and decides what actually happens.
The layersChapter 2
Clientswhat you see
Web client frontend/srcTerminal chat kreo/cli/chat.pykreo commands kreo/__main__.py
▲ ▼ HTTP /api/… and WebSocket /ws on port 8765
DoorwayAPI
FastAPI app api/app.py10 route files api/routes/WebSocket api/ws.pyRuntime api/state.py
▲ ▼ function calls
Braindecides
Orchestrator agent/orchestrator.pyRuns agent/runs.pyPermissions permissions/Scheduler scheduler/
▲ ▼
Handsdoes exact work
tasksconversationsenvironmentweatherastronomylocationweb
▲ ▼
EnginesAI and voice
LLM provider llm/providers/llama-server runtime llm/runtime/Piper voice, Whisper ears core/speech/
▲ ▼
Memorykeeps things
SQLite database/Settings config/Logs logs/

Read top to bottom for a request coming in, bottom to top for the answer going out.

Part I · The big pictureChapter 3
Chapter 3

The golden rules

A handful of rules shape every file. Once you know them, most of the design stops looking strange.

Small AI models are fluent but unreliable. A 4-billion-parameter model will happily invent a temperature, add a conversation that does not exist, or get clock arithmetic wrong. Kreo is built so that those mistakes cannot turn into actions.

The rules on the right are written in CLAUDE.md at the root of the repo. Each one exists because something went wrong without it.

A true story from the project Asked to delete one chat, the model once replied "I deleted #2 … and #3 …". That is why the deletion sentence is now written by Python and the model is told to repeat it, nothing more.
The golden rulesChapter 3

The model fills in forms

It only returns structured JSON. Python reads it and decides what happens.core/agent/schemas.py

Every figure comes from Python

Temperatures, sunrise times and search results are computed or fetched, then handed to the model to phrase.core/tools/*.py

Speech belongs to the agent

Kreo speaks with Piper and listens with faster-whisper on the agent's machine. The browser voice is only a fallback for a remote client.core/speech/

Clients attach, never start

kreo chat connects to the running agent. It never starts a second copy with its own model.cli/chat.py

One port: 8765

If the port is taken, Kreo reports it and exits with code 2. It never quietly moves.__main__.py · port_is_free

Sensitive actions ask first

They pass through the PermissionEngine and leave an AuditLog row.core/permissions/engine.py

Deleting needs a "yes" read by Python

The model can never delete by sounding confident.tools/conversations.py · is_affirmative

Secrets never touch SQLite

Credentials go to Windows Credential Manager through keyring.planned for integrations
Part I · The big pictureChapter 4
Chapter 4

Who does what: the model or Python?

This is the most important split in the whole project. The model is used for language. Python is used for everything that must be right.

Structured output means the model is not allowed to write free text for this step. llama-server receives a JSON schema and turns it into a grammar, so the model can only produce JSON that fits the form.

On the right, follow one real request. The model's only job is to fill in two small forms and then write one friendly sentence. Python checks the words, works out the time and writes the task.

The model does

  • Classify the message into an intent
  • Pull out fields: title, day, place
  • Phrase the final reply
  • Think privately first (reasoning)

Python does

  • Override the intent when it knows better
  • Work out times like "in 30 minutes"
  • Check permissions, write the audit row
  • Fetch weather, compute sunrise, search
  • Save, edit and delete in SQLite
Model or PythonChapter 4
One request, followed end to end
  1. 1
    You type "Remind me to stretch in 30 minutes" at 14:00.
  2. 2
    Model fills form 1: ParsedIntentparse_intent
    { "intent": "create_reminder", "entity": "stretch",
      "priority": "normal", "confidence": 0.9 }
  3. 3
    Python checks the wordsasks_for_written_contentNot a request to write something, so it stays a reminder. Permission create_reminder is AUTO.
  4. 4
    Model fills form 2: TaskExtraction_create_task_for
    { "title": "Stretch", "due_at": "2026-09-26T14:20:00",
      "announce": "speak" }   ← the model got the time wrong
  5. 5
    Python fixes the timeparse_relative_durationIt reads "in 30 minutes" from your own words and sets 14:30, overriding the model. Small models cannot do clock arithmetic.
  6. 6
    Task #12 is savedcreate_taskStored in UTC in tasks, kind "reminder".
  7. 7
    Model phrases the reply"Done. I'll remind you to stretch at 14:30."
Part I · The big pictureChapter 5
Chapter 5

The folder map

The repo root is the project root. The Python backend lives in kreo/ and the web client in frontend/.

Read these five files first, in this order
  1. 1
    CLAUDE.mdThe rules, and the traps found the hard way.
  2. 2
    kreo/__main__.pyThe kreo command and how the agent starts.
  3. 3
    kreo/api/app.pyHow every service is built and wired together.
  4. 4
    kreo/core/agent/orchestrator.pyThe brain: 886 lines that handle every chat turn.
  5. 5
    frontend/src/ChatContext.tsxHow the browser sends a message and shows the stream.
Not in git config/kreo.json, data/kreo.db, logs/, models/ and bin/ live on your machine only. The example config, config/kreo.example.json, is the template.
The folder mapChapter 5
Kreo-Agent/
├─ kreo/                  Python backend (the agent)
│  ├─ __main__.py          the `kreo` command
│  ├─ api/                 FastAPI app, routes, WebSocket
│  ├─ core/
│  │  ├─ agent/            orchestrator, schemas, prompts, runs
│  │  ├─ tools/            tasks, weather, sunrise, search…
│  │  ├─ speech/           Piper voice, Whisper ears, chimes
│  │  ├─ scheduler/        reminders every 20 s
│  │  ├─ permissions/      AUTO / ASK / NEVER
│  │  ├─ resources/        CPU, RAM, GPU snapshot
│  │  └─ memory/ integrations/   placeholders
│  ├─ llm/                 model providers + llama-server runtime
│  ├─ database/            SQLite tables, log sink
│  ├─ config/              typed settings, logging
│  ├─ cli/                 terminal chat client
│  ├─ windows/             toasts, start with Windows
│  └─ assets/sounds/       marimba, chime, ping
├─ frontend/              React + TypeScript + Vite
│  └─ src/  pages/  components/  styles/
├─ bin/llama.cpp/         official llama-server.exe + DLLs
├─ models/                Qwen3-4B GGUF, Piper voice
├─ config/                kreo.json (yours), kreo.example.json
├─ data/kreo.db           the SQLite database
├─ logs/                  kreo.log, llama-server.log
├─ tests/                 32 files, run with pytest
├─ benchmarks/            Stage 0 model benchmark
├─ scripts/               kreo.cmd, kreo.ps1, install-path.ps1
├─ initial-docs/          PRD, TRD, stages, design
└─ overview/              this book
Part II · Waking upChapter 6
Chapter 6

What happens when you run kreo run

The kreo command is a small launcher. Only run starts the agent. Every other subcommand talks to an agent that is already running, over HTTP.

CommandWhat it does
kreo run (or just kreo)Starts the agent. --no-model skips the AI, --open opens the browser, --reload is for development.
kreo stopFinds whoever holds port 8765 and asks it to stop. --force kills it after 10 s.
kreo statusPrints GET /api/status. --json for raw.
kreo chatTerminal chat on the same WebSocket. --voice for hands-free.
kreo listenListens once on the agent's microphone and prints the text.
kreo model on|off|toggleResume or suspend the AI model.
kreo reasoning on|off|toggleTurn the model's thinking step on or off.
kreo tasksLists tasks. --status open|all|todo|done|failed.
kreo autostart on|off|statusStart with Windows, through Task Scheduler. Works without a running agent.
kreo open [path]Opens the web client in your browser.
kreo wherePrints where the config, database, model and logs live.

scripts/kreo.cmd and kreo.ps1 run .venv\Scripts\python.exe -m kreo, and install-path.ps1 puts them on your user PATH so kreo works from any folder.

Start-upChapter 6
Start-up, in order
  1. 1
    Read the arguments__main__.build_parser
  2. 2
    Load the configload_settingsBuilt-in defaults, then config/kreo.json (or the example file), then KREO_* variables.
  3. 3
    Set up loggingsetup_loggingConsole plus logs/kreo.log, rotating at 5 MB × 5.
  4. 4
    Is port 8765 free?port_is_freeNo: print "run kreo stop" and exit with code 2. Never move to another port.
  5. 5
    Build the web appcreate_app10 routers under /api, the /ws socket, and the built client from frontend/dist.
  6. 6
    uvicorn starts serving, and the lifespan runs:
    Databaseinit, WAL
    →
    LogSinklogs → DB
    →
    ModelManager
    →
    PermissionEngine
    →
    Orchestrator
    →
    RunRegistry
    →
    Scheduler
  7. 7
    Pack them into the Runtimeapp.state.runtime
  8. 8
    Start the schedulerReminders are checked every 20 s from now on.
  9. 9
    Load the modelmodels.load("startup")Starts llama-server. If it fails, Kreo logs a warning and keeps going without a model.
  10. 10
    Warm up the earsLoads Whisper in the background so the first "listen" is quick.
  11. 11
    RUNNING"Kreo-Agent ready on http://127.0.0.1:8765"
Shutting down runs the list backwards Cancel speech warm-up → state STOPPING → cancel replies in progress ("Kreo stopped while replying.") → stop the scheduler → unload the model (stops llama-server) → stop the log sink → close the database.
Part II · Waking upChapter 7
Chapter 7

The Runtime: one box of shared services

Every service is created once, at start-up, and put into a single object called the Runtime. Any route that needs something takes it from there.

The Runtime lives at app.state.runtime. A route asks for it with FastAPI's dependency system, Depends(get_runtime) in api/deps.py, then uses rt.agent, rt.db and so on.

This means there is exactly one model manager, one database and one orchestrator, however many browser tabs or terminals are connected.

The agent's own state
STARTINGRUNNINGPAUSED · not used yetSLEEPING · not used yetSTOPPING

PAUSED and SLEEPING are waiting for the tray app (Stage 6) and the resource manager (Stage 7).

Changes made while running are not saved rt.settings is the live config. The Settings page, Permissions page and kreo autostart change it in memory only. Nothing writes config/kreo.json back, so a restart brings the file's values back.
The RuntimeChapter 7
Runtime app.state.runtime settingsKreoSettings, the live config dbasync SQLite (aiosqlite) modelsModelManager → llama-server permissionsPermissionEngine log_sinkcopies logs into SQLite agentAgentOrchestrator runsRunRegistry, replies in flight schedulerSchedulerService (APScheduler) stateAgentState started_atUTC, gives uptime_seconds Every route: rt = Depends(get_runtime) → rt.agent, rt.db, rt.models …
Defined in kreo/api/state.py. Colours follow the key: violet for Kreo's own logic, orange for the model, brown for storage.
Part II · Waking upChapter 8
Chapter 8

The model engine: llama-server

The AI model does not run inside Kreo's Python process. Kreo starts the official llama-server.exe as a child program and talks to it over HTTP, on 127.0.0.1:8766.

Why a separate program?
  • Crashes. The ready-made llama-cpp-python wheels are AVX-512 builds. They crash this Ryzen 5 4600H.
  • Security. Smart App Control is on, so unsigned DLLs without reputation are blocked. The official build is trusted.
  • Memory. Killing a process always frees the GPU memory (VRAM). That matters when a game needs the GPU.
The launch command
llama-server.exe -m models/Qwen3-4B-Q4_K_M.gguf
  -ngl 20  -c 4096  -t 6  --host 127.0.0.1 --port 8766
  -fa auto  -np 1  --jinja  --no-webui
  --reasoning-budget 640
-ngl 20Put 20 layers of the model on the GPU, the rest on the CPU
-c 4096Context window: how many tokens it can see at once
-np 1One slot, so one request at a time
--jinjaUse the model's own chat template
--reasoning-budget 640Cap on thinking tokens, so there is room left to answer
The model engineChapter 8
Kreo (python.exe) ModelManagerload · unload · suspend · resume LlamaServerProviderhttpx client → 127.0.0.1:8766 KillOnCloseJobholds the job handle Windows job object · kill on close llama-server.exebin/llama.cpp · official build Qwen3-4B-Q4_K_M.ggufmodels/ · about 2.5 GB 36 layers, drawn to scale GPU · 20GTX 1650 CPU · 166 threads 1 2 3
1 Spawn the process with no window, after killing any stale server left on port 8766. 2 Poll GET /health every second until {"status":"ok"} (240 s limit), then chat over POST /v1/chat/completions. 3 assign(pid) puts the server in a job object, so Windows kills it if Kreo dies. No orphan keeps holding the VRAM.
Which backend? build_provider(settings) decides

llama_server default

The child-process design above.llm/providers/llama_server.py

llama_cpp do not use here

Runs the model inside Python. Crashes this CPU.llm/providers/llama_cpp.py

mock tests

Replies "[mock] You said: …". Tests never need a model.llm/providers/mock.py
Part II · Waking upChapter 9
Chapter 9

Model states and the provider contract

The model can be switched off to free the GPU while the rest of Kreo keeps running. The ModelManager tracks where it is in its life.

One lock means two clicks on the model switch can never race. Every change of state calls the listeners, and one listener writes a ResourceEvent row with a CPU, RAM and GPU snapshot. You can see these on the Activity and Developer pages.

Every backend must provide these methods
MethodUsed for
load() / unload()Start or stop the model
generate(messages, thinking=…)Whole reply at once. Retries with thinking off if the answer comes back empty
stream(messages, thinking=…)Reply piece by piece as StreamDelta(text, reasoning)
structured_generate(messages, Schema)JSON that must fit a Pydantic form. Thinking off, temperature 0
embed(texts)Vectors for search. Not built yet (Stage 3)

thinking is passed on every call rather than stored on the provider, because one provider is shared by every request in flight.

Model statesChapter 9
unload() · shutdown UNLOADED LOADING LOADED ERROR SUSPENDED load() health ok fails or240 s timeout resume() suspend()frees VRAM resume() suspend()
The top bar's Model switch, kreo model on|off, and POST /api/model/toggle all call toggle(), which flips between LOADED and SUSPENDED.
How the pieces fit
ModelManagerowns the lifecycle
→
LLMProviderthe contract (base.py)
→
llama-serverdoes the maths

The rest of Kreo only ever talks to the contract, so a different engine could be swapped in without touching the orchestrator.

Part III · The life of a messageChapter 10
Chapter 10

A message's journey

This part is the heart of the book. We follow one message from your keyboard to Kreo's reply, through every file it touches.

There are two ways in. Clients normally use the WebSocket at /ws, which streams the reply as it is written. There is also POST /api/chat, which waits and returns the whole reply at once. No client uses it today, but both go through the same steps: stream() and handle() in the orchestrator.

A single turn asks the model two or three times. The first calls are quick and strict, and the last one is the friendly reply.

Model calls in one turn
1 · ClassifyParsedIntent · 256 tokens · thinking off · temperature 0
2 · Extract (sometimes)TaskExtraction, TaskAction, ConversationAction or EnvironmentQuery · 128–256 tokens
3 · Phrasethe reply you read · up to 2,048 tokens · thinking on

A "yes" or "no" answering Kreo's own question skips step 1 entirely. Python recognises it first.

A message's journeyChapter 10
  1. 1
    You press SendChatContext.sendOpens a WebSocket and sends {"type":"chat", message, conversation_id, input_mode, record, session}.
  2. 2
    The doorway accepts itws.py · websocket_endpointHands the message to rt.runs.start(). The socket only watches from now on.
  3. 3
    A Run is createdruns.py · RunRegistryThe reply becomes a background task owned by the agent, so closing the tab loses nothing.
  4. 4
    Your message is saved_ensure_conversation · _storeNew conversation if needed, titled from your first 120 characters.
  5. 5
    Quick Python checksis_farewell · _answer_to_pending_deleteDid you say goodbye? Are you answering "Shall I delete…?"
  6. 6
    The model classifies itparse_intentOne of 19 intents, as JSON.
  7. 7
    Permission checkpermissions.evaluateAUTO runs, ASK waits for approval, NEVER refuses.
  8. 8
    Python does the work_performCreates the task, fetches the weather, searches… and returns an Outcome with a note.
  9. 9
    The note is framed_with_outcomeReference, completed action, or a sentence to say.
  10. 10
    The model writes the replyprovider.streamThinking arrives as reasoning events, the answer as token events.
  11. 11
    You watch it appearIn voice mode, each finished sentence is already being spoken.
  12. 12
    Everything is stored_store · _metric · _auditReply, speed figures and audit row. Deletions run last. Then end.
Part III · The life of a messageChapter 11
Chapter 11

The same journey as a sequence diagram

Time runs downwards. Solid arrows are calls, dashed arrows are answers and events. The WebSocket carries small JSON events, in this order:

EventWhenCarries
runAlways firstrun_id, so a client can reattach
startConversation knownconversation_id, input_mode, recorded
farewellYou said goodbyeEnds hands-free mode after speech
intentAfter classifyinge.g. create_reminder
taskA task was touchedtask_id, kind, action
sourcesA web search ranTitle, URL, domain, snippet
reasoning ×nWhile thinkingPieces of the hidden thinking
token ×nWhile answeringPieces of the answer
conversation_deletedAfter storingids, included_current
endAlways lastmessage_id, latency_ms, tokens_per_second
errorAnything failedA readable message

The docstring at the top of ws.py still leaves out sources and conversation_deleted, but both are sent.

Sequence diagramChapter 11
Browser ws.py RunRegistry Orchestrator Model Tools SQLite {type:"chat"} runs.start() event: run stream() store user message event: start classify ParsedIntent permission check event: intent _perform() Outcome(note) stream(+note) deltas… events: reasoning, token, token… store reply · metric · audit event: end Events travel Orchestrator → RunRegistry → ws.py → Browser; drawn direct for clarity.
If the model is switched off, the path is short: start, one token saying the model is unavailable, end. If the permission is NEVER: intent, a refusal token, end.
Part III · The life of a messageChapter 12
Chapter 12 · Step 1

Understanding the message

First, the model sorts your message into one of 19 intents. It fills in a form called ParsedIntent.

FieldMeaning
intentWhich of the 19 kinds (default chat)
entityThe thing it is about, e.g. "stretch"
prioritylow, normal, high or urgent
requires_approvalThe model's guess. Python ignores it and asks the PermissionEngine
argumentsExtra text fields
confidence0 to 1
Python checks the model's homework
asks_about_rather_than_requestsA message that starts with a question word ("When is my…?") never creates a task.
asks_for_written_content"Create a whole day routine for me" is a request to write something, so it becomes chat, not a task. Unless the word "remind" appears.
If the model failsAny error while classifying gives unknown, which is treated as chat. Every call writes a GenerationMetric row.
The 19 intentsChapter 12
The 19 intents, grouped by what happens

Tasks and reminders

create_remindercreate_taskcreate_open_loop
Create a row in tasks. An open loop becomes kind "job" for now.
update_taskcomplete_taskcancel_taskdelete_task
Find the task (by id or fuzzy title) and change it.
list_tasksquery_memory
Both list your open tasks until the memory system exists.

Facts from the world

weathersun_timesweb_search
Python fetches or computes the facts. The model only phrases them.

Conversations

delete_conversation
Two-step: Kreo asks, you say yes, Python deletes. See chapter 20.

Just talk

chatsummarisedraft_replyrememberunknown
No tool runs. The model answers from the conversation alone.

Planned

schedule_meeting
Its permission is ASK, so today Kreo explains it needs approval. The calendar arrives in Stage 9.
Intent → permission action
create_reminder, create_task, create_open_loopcreate_reminder
update_task, complete_task, cancel_taskupdate_task
delete_task · delete_conversation · web_searchsame name
draft_reply · schedule_meetingdraft_email · schedule_meeting
everything elseno check needed
Part III · The life of a messageChapter 13
Chapter 13 · Step 2

The switchboard: _perform

With the intent known and the permission checked, one method decides which piece of Python runs. It always returns an Outcome.

The Outcome, a small parcel of results

@dataclass
class Outcome:
    note: str | None              # what the model should know
    task_id: int | None           # task created or changed
    sources: list[dict]           # web sources to cite
    note_is_instruction: bool     # note is a sentence to say
    delete_conversations: list    # ids to delete at the very end
No tool registry Many agent frameworks let the model pick tools by name. Kreo does not. tools/__init__.py is only a docstring. The orchestrator imports each tool module and calls its functions directly, so the model can never call a tool.
The switchboardChapter 13
Which branch runs
If…Python doesOutcome
ASK permissionNothingInstruction: "I haven't done that. It needs your approval first."
create_*_create_task_for: guards → TaskExtraction → create_tasktask_id, no note
update / complete / cancel / delete task_edit_task_for: recent tasks → TaskAction → resolve_task → change ittask_id + fact: "Marked #4 … as done."
delete_conversation_stage_conversation_delete: nothing deleted yetInstruction: "Shall I go ahead?"
list_tasks · query_memory_list_tasks_noteFact: "The user's open tasks are: …"
weather · sun_times_environment: EnvironmentQuery → environment.answerReference fact with real figures
web_search (if enabled)_search_web: DuckDuckGo → read pages → number sourcesReference + sources
anything elseNothingEmpty: the model just chats
Then the stream announces what happened
event: taskif a task changed
+
event: sourcesif the web was searched
→
phrasing callchapter 14
Part III · The life of a messageChapter 14
Chapter 14 · Step 3

Framing the note

The Outcome's note is added to the prompt as one extra system message. How it is worded changes how a small model behaves, so there are three frames.

The prompt sent for the reply is: Kreo's persona (SYSTEM_TEMPLATE, with today's date and time), then recent history, then the framed note.

History is cut to 4 messages when there is a note, so the facts are not drowned out, and 20 otherwise.

Wrong frame

Search results framed as an action already done:"I performed a web search and found several results about the weather…"

Right frame

Framed as reference material:"It's 18 °C and cloudy in Pune today [1]."
Why "say()" exists Given an instruction like "tell the user nothing matched", a 4B model reads the instruction aloud or invents detail around it. So Python writes the exact sentence with conversations.say() and sets note_is_instruction.
Three framesChapter 14

Reference weather · sun_times · web_search

{note}
Answer the user's question using only the information above.
Never mention the search… State the answer as plain fact in
the first sentence.

Completed action task edits, task lists

Kreo has already carried this out: {note}
Report exactly this to the user. Do not invent any other detail.

Instruction note_is_instruction = True

Reply with exactly this, rephrased only lightly if you must:
"Just to check — that will delete "Trip ideas", and it can't be
undone. Shall I go ahead?"
Add nothing else. Do not mention these instructions. Do not
invent any other conversation, number or detail.
Built by say() and added as it stands.

Notes longer than the context budget (about 6,451 characters) are cut at a word boundary and end with "…[truncated to fit the context window]".

Part III · The life of a messageChapter 15
Chapter 15 · Step 4

Writing the reply, and token budgets

A token is a piece of a word, roughly three or four characters. Every model call has a budget of tokens, and Kreo sets each one on purpose.

Qwen3 can think before answering. The thinking and the answer come out of the same budget, max_tokens = 2,048. Without a cap, the model could think until the budget ran out and answer nothing. So llama-server is started with --reasoning-budget 640.

The thinking is streamed as reasoning events. The client shows it in a folded "Thinking…" block above the answer.

If the answer still comes back empty
Replyreasoning only, no answer
→
Python noticesanswer is blank
→
Ask againthinking=False
→
You see an answernever an empty bubble

For streaming this retry lives in orchestrator.stream. For whole replies it lives in LlamaServerProvider.generate.

Token budgetsChapter 15
One reply: max_tokens = 2,048 (to scale)
Thinking ≤ 640
Answer: at least 1,408 left
The context window: 4,096 tokens (to scale)
Note: up to 45%
persona, history and the reply share the rest
Note budget = max(800, 4,096 × 3.5 characters × 0.45) ≈ 6,451 characters.
Token cap for each kind of call (to scale, out of 2,048)
01,0242,048 Phrase the reply2,048 Classify (intent)256 Task extraction256 Task action256 Conversation action192 Environment query128
Violet bars are strict JSON calls: thinking off, temperature 0, output constrained by a JSON-schema grammar. The orange bar is the friendly reply, with thinking on and temperature 0.3.
Part III · The life of a messageChapter 16
Chapter 16

Runs: replies that survive a closed tab

A chat turn belongs to the agent, not to the browser tab that asked for it. That work unit is called a Run.

An early version drove the orchestrator straight from the WebSocket handler. Closing the tab aborted the reply halfway, and it was never saved. Now RunRegistry.start() launches the reply as its own background task.

A Run keeps every event it has produced. Anyone who subscribes gets the full replay first, then follows live. Each subscriber sees every event exactly once.

The same idea exists on the client: the conversation lives in ChatProvider, above the router, so moving to another page does not kill the reply.

Client sendsMeaning
chatAsk something new
attach + run_idRejoin a reply. Unknown run: gone
answer + conversation_idAnswer a question already stored but never answered (store_user=False, so it is not saved twice)
forget + sessionDrop an off-the-record thread
pingServer answers with status
RunsChapter 16
One Run and its event tape
runstartintentreasoningreasoningtokentokentokentokentokenend
Run a1b2c3d4e5f6 · owned by the agent · kept 5 minutes after it finishes Tab A live from the first event tab closed: the reply keeps going and is saved Tab B not open yet attach replay so far then live On start-up the client asks GET /api/chat/active and reattaches to any reply still being written.

Inside a Run

12-character id, the prompt, conversation_id, every event so far, subscriber queues, done, error, the asyncio task.core/agent/runs.py

Tidy-up

Subscribers are closed with contextlib.aclosing. Otherwise an abandoned queue keeps filling until garbage collection.RunRegistry.events
Part IV · The toolsChapter 17
Chapter 17

The toolbelt

Tools are ordinary Python modules in kreo/core/tools/. They do the exact work: dates, databases, maths and web requests.

A tool never decides what to do. The orchestrator calls it after classifying your message. The tool returns facts, and the facts come back to the model as a note to phrase.

There are two kinds of note, and choosing the right one matters (see chapter 14):

A fact

"Marked #4 Call the bank as done."
"Sunrise 06:14, sunset 18:21 … Figures from Open-Meteo."

A sentence to say

Built with say(), with note_is_instruction = True.
"Just to check — that will delete…"
Honesty rule If a tool fails, the note says so plainly ("could not be reached"). Kreo would rather say it does not know than invent a figure.
The toolbeltChapter 17

tasks.py 441 lines

Create, edit, complete and delete tasks and reminders. Regular expressions and Python decide what is written.no network · SQLite

conversations.py 292

Two-step deletion, "yes/no" and goodbye detection, and say().no network · SQLite

environment.py 73

Answers weather and sunrise questions by combining the three tools below.coordinator

weather.py 159

Daily and current weather turned into one factual sentence.Open-Meteo forecast + archive · no key

astronomy.py 143

Sunrise, sunset, solar noon, twilight, from NOAA's solar equations.offline maths

location.py 135

Where are you? A named place, then your home setting, then an opt-in IP lookup.Open-Meteo geocoding · ipapi.co · ip-api.com · ipwho.is

web.py 218

Search, read the top pages, lay out numbered sources for citations.DuckDuckGo (ddgs) · httpx · BeautifulSoup
Part IV · The toolsChapter 18
Chapter 18

Tasks and reminders

Every task, reminder and job is one row in the tasks table. You can create them by talking to Kreo, on the Tasks page, or with POST /api/tasks.

A task's life
todo
→
in_progress
→
done
or
cancelled
or
failed

The API keeps started_at and completed_at in step with the status.

ColumnValues
kindtask · reminder · job
prioritylow · normal · high · urgent
due_atWhen it is due, stored in UTC
fired_atSet once the reminder has gone off, so it never fires twice
announcementThe sentence Kreo will say out loud
announce_modespeak (sound, toast, voice) · notify (sound, toast) · silent (list only)
Tidying up Every 30 minutes, finished tasks older than 24 hours are removed. High and urgent tasks, jobs, and tasks with a description are always kept.
TasksChapter 18
Creating: "Remind me to take the bins out at 19:30"
  1. 1
    GuardsNot a question, not a request to write something.
  2. 2
    Model fills TaskExtractiontitle, description, due_at, priority, spoken_announcement, announce.
  3. 3
    tidy_titleStrips "remind me to" and "at 19:30": the title becomes "Take the bins out".
  4. 4
    parse_relative_durationFor "in 30 seconds / minutes / hours", Python's own sum replaces the model's time.
  5. 5
    parse_dueDrops any timezone the model added, treats the time as local, converts to UTC.
  6. 6
    default_announcementIf the spoken line just repeats the title, a better one is written.
  7. 7
    create_taskRow saved. The client gets a task event and shows a link.
Editing: "Mark the bins one as done"
recent_tasksthe last few open tasks
→
TaskActiontask_id, title_hint, new_title, new_due_at…
→
resolve_taskid if it exists, else fuzzy title match (difflib, 0.6)
→
set_task_statusor update / delete

The reply gets a fact note such as "Marked #7 Take the bins out as done." If nothing matches, Kreo says so instead of guessing.

Part IV · The toolsChapter 19
Chapter 19

Weather, sunrise and location

Ask "Will it rain in Manchester tomorrow?" or "When does the sun set?" Every number in the answer is fetched or computed in Python.

The model fills in a tiny form, EnvironmentQuery, with just two fields: day (an ISO date, or empty for today) and location (empty means where you are).

Sun times need no internet at all. astronomy.py uses NOAA's solar equations and handles polar day and polar night.

Limits Python enforces
Earliest day: 1 January 1940 Forecast up to 16 days ahead Older than 6 days: archive endpoint
Set your home Settings → This machine → Location. PATCH /api/location sets it for this run, POST /api/location/detect looks it up from your IP, and GET /api/location/sun shows today's sun times.
Weather and sunriseChapter 19
Where are you? Tried in this order
1 · A place you namedgeocoded by Open-Meteo
→
2 · Your home settinglatitude, longitude, timezone
→
3 · IP lookuponly if auto_detect is on
→
Unknownnote asks you to set it in Settings
Then two sources are combined

astronomy.py

Sunrise, sunset, solar noon, day length, first and last light. Local maths, local time.

weather.py

Weather code, max and min temperature, rain, max wind, sunrise and sunset; plus current temperature when the day is today.
The note the model receives (reference frame)
Weather for Manchester on Saturday 27 September 2026: light rain,
high 16 °C, low 9 °C, 4.2 mm of rain, wind up to 24 km/h.
Sunrise 07:02, sunset 18:58. Figures from Open-Meteo.

Example of the note's shape. The model is told to answer only from this, as plain fact in the first sentence. If Open-Meteo cannot be reached, the note says so and no figure is given.

Part IV · The toolsChapter 20
Chapter 20

Deleting conversations safely

Deleting is the one thing that cannot be undone, so it always takes two turns. The "yes" is read by Python, never by the model.

Which conversations? resolve_targets
currentThis chat
namedBy number, then title, then fuzzy title (0.6). Any number you say means "named"
allOnly if you clearly say "all"
What counts as "yes"? is_affirmative

An exact yes-phrase, or up to 7 words starting with a yes-word and containing none of: but, not, except, instead, other, another, rather, unless, only, first, actually, wait, no. "Yes but only the old one" is not a yes.

The delete itself, in one database session
  1. 1
    Delete that conversation's generation_metrics
  2. 2
    Detach its tasks (conversation_id → NULL). Tasks survive.
  3. 3
    Delete its messages
  4. 4
    Delete the conversations row, then write an audit row
Two-step deleteChapter 20
YouDelete the trip ideas chat.
intent = delete_conversation → ConversationAction {scope:"named", title_hint:"trip ideas"}
PendingDeletion stored in memory for this conversation · expires in 5 minutes · nothing deleted
KreoJust to check — that will delete "Trip ideas" (14 messages), and it can't be undone. Shall I go ahead?
YouYes please.
_answer_to_pending_delete runs before classification · is_affirmative("yes please") = True
KreoDone — I've deleted "Trip ideas".
reply stored and streamed FIRST → then _apply_deletions → event: conversation_deleted → end

You say "no"

"Right, I've left everything where it is."

You say something else

The pending delete is dropped quietly and your message is handled normally. Kreo never asks twice.
Why delete last? The reply is stored in the conversation being deleted. Deleting first would lose the reply. If the current chat was deleted, the client clears it after 3.5 seconds.
Part IV · The toolsChapter 21
Chapter 21

Web search with citations

When you ask about something recent, Kreo searches DuckDuckGo, reads the top pages, and answers with numbered citations such as [1].

Safety checks in web.py
  • is_public_http_url refuses localhost, private, loopback, link-local, reserved and multicast addresses. A web page cannot trick Kreo into reading your router or your own API.
  • Each page download stops at 2 MB.
  • Only readable text is kept, up to 1,800 characters a page.
Settings (web group)
enabled
on. Off means web_search falls back to chat
max_results
5 search results
fetch_top
2 pages actually read
max_page_chars
1,800
timeout_seconds
15

Permission action web_search is AUTO by default. If nothing useful comes back, the note tells the model to say it could not find it.

Web searchChapter 21
query_fromyour words → a search query
→
searchDuckDuckGo, 5 results
→
fetchtop 2 pages (httpx)
→
_extract_textBeautifulSoup + lxml
→
format_for_promptnumbered sources
What the model sees
[1] Met Office — Manchester forecast (metoffice.gov.uk)
    Snippet… Page text…
[2] BBC Weather — Manchester (bbc.co.uk)
    Snippet… Page text…
Cite as [1]. Answer first. No bullet points.
What you see
Light rain is expected in Manchester tomorrow afternoon, clearing by evening [1][2].
1 M metoffice.gov.uk 2 B bbc.co.uk Show details
Source chips come from the sources event, drawn by components/Sources.tsx. Letter icons are drawn locally; no favicon is fetched. They are also saved in the message's metadata.
Part IV · The toolsChapter 22
Chapter 22

Off the record

Kreo's answer to incognito mode. Flip the switch in the chat toolbar and the conversation is held in memory only. It is never written to the database.

The client sends record: false and a random session id with each message. The orchestrator creates no conversation, and the thread lives in EphemeralStore (core/agent/ephemeral.py).

When you switch back on the record, the client sends {"type":"forget"} and the thread is dropped at once. A restart wipes all of them.

The whole chat turns graphite, so you cannot forget which mode you are in.

Memory limits
40
messages per session
20
sessions at once
2 h
unused, then forgotten
Off the recordChapter 22
What gets written down?
ThingNormalOff the record
Conversation row✓ saved✗ none
Your messages and Kreo's replies✓ saved✗ memory only
Tasks and reminders you ask for✓ saved✓ saved: you asked for them
Audit row for a sensitive action✓ with your words✓ reason replaced by "off the record"
Speed metric (no content)✓✓
Prompt shown in /api/chat/active✓✗ hidden
Can be reattached after a refresh✓only within the session
Your message Orchestrator SQLiterecord: true EphemeralStorerecord: false · RAM only
Part V · Working in the backgroundChapter 23
Chapter 23

The scheduler: reminders that fire on time

Kreo keeps working with no browser open. A background timer, APScheduler, checks for due reminders every 20 seconds. It never calls the AI model.

The two jobs

due-tasks · every 20 s

Find tasks that are due now, not fired yet, still open, and no more than 2 days late.check_due_tasks

prune-tasks · every 30 min

Remove old finished tasks (chapter 18).prune_finished_tasks

Both run with max_instances=1 and coalesce=True, so a slow run never piles up behind itself.

At most once fired_at is set and the Notification row is written in the same transaction. Even if something crashes during delivery, a reminder can never fire twice.
Quiet hours: 23:00 to 07:00 They silence the sound and the voice. The toast and the notification row still happen, so nothing is lost.
The schedulerChapter 23
14:30:00 14:30:20 14:30:40 14:31:00 due 14:30:07 delivered here Reminders land within 20 seconds of their time.
Delivering one reminder
  1. 1
    Mark fired + add a NotificationOne transaction: level="reminder", source="scheduler".
  2. 2
    Windows toastWith an "Open Kreo" button to /tasks, if notifications.enabled. Result saved as delivered / delivery_error.
  3. 3
    Quiet hours or silent?Then stop here.
  4. 4
    Play the chimemarimba (default), chime or ping, made by scripts/make_sounds.py.
  5. 5
    Speak the announcementIf the mode is speak and spoken reminders are on: Piper reads it on the agent's speakers.
  6. 6
    Tell listenersThe bell in the top bar picks up the unread count on its next status poll.
Part V · Working in the backgroundChapter 24
Chapter 24

Permissions and the audit log

Every action with consequences has a permission level. The PermissionEngine looks it up before anything happens.

The built-in table merges with your config
Built-in tablein settings.py
+
Your overridespermissions.defaults in kreo.json
=
Effective tablenothing ever missing
A bug this prevents The config used to replace the table. An action added to the code later, like delete_conversation, was missing from older config files and fell back to ASK. The agent then did nothing while claiming success.
Every audit row records
action
e.g. create_reminder
source · tool
chat · orchestrator
reason
your words (up to 2,000 characters), or "off the record"
result
"task #12", "reply generated", "deleted 1 conversation(s)", "denied"
approval
approval_required, approval_status: pending or not_required

Changes made on the Permissions page are held in memory until restart. Saving them to the database is planned for Stage 12.

PermissionsChapter 24

AUTO Kreo just does it

create_reminderupdate_taskdelete_taskdelete_conversationweb_searchdraft_emailsummarize_slack
Runs, then writes an audit row.

ASK Kreo waits for you

schedule_meetingsend_routine_replysend_sensitive_documentfinancial_transactionany unknown action
Nothing runs. Kreo says it needs approval. Audit: approval_status = pending. An approval screen comes in Stage 12.

NEVER Kreo refuses

delete_data
"I am not permitted to perform…". The refusal is stored and audited as "denied".
Where you see it
Permissions pageActivity timelineDeveloper → auditGET /api/permissions · PUT /api/permissions/{action} · GET /api/dev/audit
Part V · Working in the backgroundChapter 25
Chapter 25 · partly built

Resource awareness

The aim: Kreo gets out of the way when your PC is busy. Start a game and the model goes to sleep. Stop playing and it comes back.

Built today: snapshot() in core/resources/state.py reads CPU, RAM, GPU, VRAM, the app in front, and the battery. You see it as meters on the Overview page, and a snapshot is saved with every model state change.

Not built yet (Stage 7): the loop that watches these numbers and suspends the model on its own. /api/status always reports ACTIVE for now, and gaming_detected is always false.

You can already free the GPU by hand: the Model switch, or kreo model off.

Where the numbers come from
CPU, RAM, battery
psutil
GPU, VRAM
NVIDIA's NVML (pynvml), which gives up after 3 failed starts
App in front
Win32 GetForegroundWindow
ResourcesChapter 25
Thresholds that will trigger the sleep (red marks)
GPU use
40%
CPU use
70%
RAM use
85%
From ResourceSettings: poll every 5 s, and wait 60 s of calm before resuming. Profiles: performance, balanced (default), resource_saver, gaming, manual.
ACTIVE RESOURCE_SAFE GPU_SUSPENDED RESUMING 60 s of calm busy PC game detected Green = in use today. Dashed = defined as types only, waiting for Stage 7.
Part VI · VoiceChapter 26
Chapter 26

How Kreo speaks

Kreo's voice comes out of the speakers of the machine running the agent. The browser never speaks; it only asks the agent to.

The voice is Piper, a small neural text-to-speech engine that runs on the CPU. The model is en_US-l2arctic-medium (about 77 MB). It has many speakers, and four of them are native Hindi speakers of English, which gives Kreo its Indian accent. Piper publishes no Indian English voice of its own.

If Piper is missing or fails, Kreo falls back to the Windows voice (SAPI), driven through a short PowerShell command.

KeySpeaker idVoice
svbi2Female. Default for the female persona
tni9Female, softer
asi10Male. Default for the male persona
rrbi19Male, deeper
Stop means stop, mid-word POST /api/speech/stop bumps a generation counter. Every sentence queued before the stop sees the new number and gives up. Playback is purged and any SAPI process is killed.
SpeakingChapter 26
From streaming text to sound, one sentence at a time
  1. 1
    Tokens stream inChatContextEach token is pushed into the SpeechQueue while you read.
  2. 2
    Cut into sentencesspeech.ts · SpeechQueueA sentence ends at . ! ? followed by a space, so "3.5" stays whole. At least 16 characters.
  3. 3
    Clean for speakingforSpeechRemoves [1] citations, Markdown symbols and URLs.
  4. 4
    POST /api/speech/speakOne sentence per request. It returns only when the sentence has finished playing, so sentences never overlap and need no timers.
  5. 5
    tts.speak picks the enginecore/speech/tts.pyPiper first (if installed and not set to "sapi"), otherwise SAPI.
  6. 6
    Piper renders a WAVpiper_engine.synthesiseVoice loaded once and cached. Text capped at 600 characters. File in %TEMP%.
  7. 7
    winsound plays itsounds.play_wavOn the agent's speakers. The WAV is deleted afterwards.
Result The answer starts being read aloud while the model is still writing the rest of it.
Part VI · VoiceChapter 27
Chapter 27

How Kreo listens

Listening also happens on the agent's machine. The client calls POST /api/speech/listen, and the agent records from its own microphone. The browser never records or uploads any audio.

The recogniser is faster-whisper with the base.en model (about 75 MB), running on the CPU in int8. It is loaded once, and warmed up at start-up so the first request is quick.

A lock allows one recording at a time. A second client gets "Kreo is already listening somewhere else".

Recording rules
Format
16 kHz, mono, 50 ms blocks
Room check
first 4 blocks; threshold = max(0.006, loudest × 3)
Stop
1.2 s of quiet after speech
Give up
8 s with no speech, or 30 s in total
Too short
under 0.3 s counts as "nothing was said"
Transcribe
English, voice-activity filter on, beam size 1

When the client is on a different machine from the agent, it falls back to the browser's own recogniser. Chrome and Edge send that audio to the browser vendor, and the code says so.

ListeningChapter 27
speech threshold room check you speak 1.2 s quiet stop → transcribe 0 s123456
An illustrative recording, drawn to the real timings. Orange bars measure the room noise, violet bars are speech, and grey is quiet.
ClientPOST /api/speech/listen
→
sounddeviceagent's mic
→
faster-whisperbase.en · CPU · int8
→
{ok, text}becomes your message

Also used by the microphone button in text mode, by kreo listen, and by /mic in kreo chat.

Part VI · VoiceChapter 28
Chapter 28

The hands-free loop

The chat has two modes, text and voice, and you can switch in the middle of a conversation. Voice mode is a loop: listen, think, speak, listen again.

The loop lives in useVoiceLoop.ts. Instead of each step starting the next with callbacks, one rule is checked whenever anything changes. If the rule says "listen" and nothing is listening, it starts. If it says "don't", it stops.

The microphone is closed while Kreo speaks, so it cannot hear itself and answer itself.

Saying goodbye ends the session. Python spots it with is_farewell, the stream sends farewell, and the client leaves voice mode 700 ms after the last word is spoken. The model can never end a session by sounding final.

Which microphone?
agentif listening_available
else
browserif SpeechRecognition exists
else
none

Messages sent by voice are stored with input_mode = "voice", and the chat shows a "spoken" mark under them.

Hands-freeChapter 28
The one rule shouldListen = supported && mode === 'voice' && !busy && !speaking
Listening mic open Thinking busy = true Speaking speaking = true you stop talking → send(text) first sentence ready → SpeechQueue last sentence played, reply finished → listen Heard nothing? The loop simply listens again. Say "goodbye" to leave.
The orb on screen (KreoOrb) shows the same states: idle, listening, thinking, speaking, error.
The full-screen voice overlay (LiveVoice)
MuteStop Kreo talkingLeaveshows what it heard and the last reply
Part VII · Where things are keptChapter 29
Chapter 29

The database

Everything Kreo remembers lives in one SQLite file, data/kreo.db. There are 21 tables. Eight are in use today; the rest are ready for later stages.

How it is set up (database/engine.py)
  • Async. SQLAlchemy with aiosqlite, so a database call never blocks the chat stream.
  • WAL mode and foreign keys on.
  • Home-made migrations. No Alembic. On start-up, _add_missing_columns compares the models with the file and adds any new columns. Dropping or retyping a column would need a real migration.
  • Full-text search. FTS5 indexes messages_fts and memories_fts, kept in step by triggers.
  • Always UTC. SQLite stores no timezone, so everything is written in UTC and tagged on the way out by iso_utc().
Waiting for later stages
memoriespeopleprojectsopen_loopsemailsslack_messagescalendar_eventspermissionsagent_actionseventsuserssettingsmodel_state
The databaseChapter 29
conversations idtitle · sourcearchivedcreated_at · updated_at messages idconversation_id →role · contentreasoning · intentinput_modelatency_ms · tokensmetadata (JSON) tasks id · titleconversation_id →kind · statuspriority · due_atfired_atannouncementannounce_mode notifications id · title · bodytask_id →level · readdelivereddelivery_error generation_metrics conversation_id →message_id →kind · modellatency_ms · tok/sok · error standalone logs audit_logsensitive actions resource_eventsmodel state changes log_recordsapp logs, newest 50,000 Arrows point from the many side to the one it belongs to (a foreign key). The ORM declares no relationship() objects; tables are linked by these id columns only. Deleting a conversation detaches its tasks rather than deleting them.
Part VII · Where things are keptChapter 30
Chapter 30

Configuration and secrets

Every setting has a typed default in kreo/config/settings.py. Your own values go in config/kreo.json, which git ignores.

If kreo.json does not exist, Kreo uses config/kreo.example.json. When you add a new setting to the code, add it to the example file too.

Environment variables start with KREO_ and use a double underscore for nesting: KREO_SERVER__PORT=9000 means server.port.

Gotcha: the file beats the environment The docstring says environment variables win. In practice, values from the JSON file are passed in a way pydantic-settings ranks higher. So a KREO_* variable only fills in keys the file leaves out.
Secrets Passwords and tokens never go in SQLite or the config file. They belong in Windows Credential Manager, through the keyring library. Email, calendar and Slack will use this.
ConfigurationChapter 30
Where a setting's value comes from
1 · Built-in defaultssettings.py Pydantic models
2 · config/kreo.jsonor the example file. Permission defaults are merged over the built-in table
3 · KREO_* variablesonly for keys the file leaves out
4 · Changes while runningSettings, Location, Permissions pages. Memory only, gone on restart
The settings groups

server

host 127.0.0.1 · port 8765

ai

provider · model_path · context 4096 · gpu_layers 20 · threads 6 · max_tokens 2048 · reasoning_budget 640 · thinking · server_port 8766

speech

engine auto/piper/sapi · persona · speaker · length_scale · recognise_model base.en · listen_silence 1.2 s

notifications

enabled · sound · speak_reminders · quiet hours 23:00–07:00 · voice · rate

permissions

defaults: action → auto / ask / never

web · location · tasks

search limits · home place, auto_detect · auto-prune after 24 h

resources · startup

thresholds, profile · start_with_windows, open_client

database · logging

data/kreo.db · level INFO, logs/
Part VII · Where things are keptChapter 31
Chapter 31

Logs and performance metrics

Kreo keeps two kinds of records about itself: logs (what happened) and metrics (how fast the model was).

The text file logs/kreo.log is the source of truth. A copy of each log line also goes into SQLite, so the Developer page can search and filter them.

llama-server writes its own output to logs/llama-server.log. If the model fails to start, look there first.

A GenerationMetric row for every model call
intenttask_extractiontask_actionconversation_actionenvironmentweb_searchchatchat_stream

Each row has latency, prompt and completion tokens, reasoning length, tokens per second, and whether it worked. The stream reports no token usage, so for streamed replies tokens are estimated as characters ÷ 4.

Logs and metricsChapter 31
Where one log line goes
  1. 1
    logger.info("…")Anywhere in kreo/.
  2. 2
    Console and logs/kreo.logRotating, 5 MB × 5 files.
  3. 3
    BufferingLogHandlerKeeps up to 5,000 lines in memory. Never touches the database itself. Skips SQLAlchemy's own logs to avoid a loop.
  4. 4
    LogSink flushes every secondOne batch insert into log_records. Every 60th flush keeps only the newest 50,000 rows.
  5. 5
    Developer → LogsGET /api/dev/logs?level&q&logger, refreshed every 5 s.
Where the metrics show up

Analytics page

Ranges 1 h to 30 d. Latency and tokens-per-second charts, p50 and p95, log levels.GET /api/dev/metrics

Under each reply

Time, intent, latency, tokens per second, task link.end event · messages table
Part VIII · The clientsChapter 32
Chapter 32

The web client

The browser app is React 19 and TypeScript, built with Vite. FastAPI serves the built files from frontend/dist, on the same port as the API.

The stack
React 19.2react-router-dom 7Vite 8TypeScript 6lucide-react iconsreact-markdown + GFMInter fontno state libraryno CSS frameworkno chart library

State lives in React Context. Styles are plain CSS with design tokens in index.css: violet marks AI presence, everything else stays neutral. Charts are hand-drawn SVG.

Keeping the browser up to date
  • index.html is served with no-cache headers. The JS and CSS files have content hashes in their names, so they can be cached safely.
  • /api/status reports which bundle is live. If the page is running an older one, StatusContext reloads it once.
  • In development, npm run dev serves on port 5173 and proxies /api and /ws to 8765.
The web clientChapter 32
Component tree
main.tsx
 └ StrictMode → BrowserRouter → App
    └ ThemeProvider          light / dark / system
       └ StatusProvider      polls /api/status every 4 s
          └ ConfirmProvider  in-app "Are you sure?"
             └ ChatProvider  the conversation, above the routes
                └ Shell
                   ├ Sidebar        11 pages, badges, theme
                   ├ app-main
                   │  ├ TopBar      title, search, bell, Model switch
                   │  └ ErrorBoundary → Routes → pages/*
                   └ CommandMenu    Ctrl+K
Why ChatProvider sits so high If the chat page owned the conversation, moving to another page would unmount it and kill the reply. Above the router, a reply keeps arriving wherever you are.
Why the ErrorBoundary sits inside A React render error blanks the whole window, navigation included. Inside the router, one broken page cannot take the rest with it. It resets when you change page.
Part VIII · The clientsChapter 33
Chapter 33

The eleven screens

The sidebar lists eleven pages in three groups. Two are honest placeholders for features that are not built yet.

Always there

Top bar

Page title, Search (Ctrl K), the notification bell with unread count, and the Model on/off switch. Shows the model's last error.

Sidebar

Agent status dot, badges for open tasks and errors, a pulsing dot on Chat while Kreo replies, theme toggle, host:port and uptime.

Command menu

Ctrl+K searches pages, tasks and conversations. Arrow keys, Enter, Escape.

Status

StatusContext polls every 4 s: agent state, model, resources, scheduler jobs, counts.
ScreensChapter 33
RoutePageWhat you do there
/OverviewGreeting, figure tiles, needs attention, 24 h latency chart, CPU/RAM/GPU meters, tasks due soon, recent chats
/chatChatTalk to Kreo. Thinking block, Markdown, sources, reply details, off-the-record switch, hands-free mode
/tasksTasksOpen / All / Done / Failed tabs, quick add, Start, Complete, Cancel, Reopen, Retry, Delete
/activityActivityOne timeline of audited actions, runtime events and reminders
/automationsAutomationsScheduler jobs with next run times, reminders waiting to fire
/memoryMemory planned"Not built yet" banner, counts, stored conversations
/integrationsIntegrations plannedWhat is connected now, what is coming (email, calendar, Slack)
/analyticsAnalyticsModel speed and latency over time
/permissionsPermissionsAuto / Ask / Never per action, recent audit entries
/devDeveloperFull transcripts with reasoning, searchable logs, runtime events
/settingsSettingsTheme, start with Windows, location, Kreo's voice, reminder sound, spoken reminders, quiet hours
Part VIII · The clientsChapter 34
Chapter 34

ChatContext: the client's side

ChatContext.tsx (681 lines) owns the conversation in the browser: the messages, the socket, voice mode, dictation and off-the-record mode.

What happens when you press Send
  1. 1
    Trim the text. Give up if it is empty or a reply is already running.
  2. 2
    Silence any speech and clear the box. Remember if voice mode is on.
  3. 3
    Add two bubbles at once: yours, and an empty Kreo bubble marked pending.
  4. 4
    Open a new WebSocket for this turn and send {type:"chat"}.
  5. 5
    Each event updates the last bubble (see right).
  6. 6
    On end: close the socket, reload the conversation list.
Rejoining after a refresh On start-up the client calls /api/chat/active. If a reply is still being written, it loads the stored messages and sends attach. A rejoined reply is not read aloud.
ChatContextChapter 34
Each event and what it changes on screen
EventOn screen
startSwitches to the new conversation. A guard stops the reload from wiping the live reply
intent · taskStored on the bubble; a task shows as a link
reasoningGrows the folded "Thinking…" block
sourcesAdds numbered source chips. Added, not replaced: one turn can search twice
tokenAppends to the answer. In voice mode also feeds the SpeechQueue
conversation_deletedShows a notice; clears the thread after 3.5 s if it was this one
farewellLeaves voice mode once speech has finished
endClears pending, shows latency and speed, speaks the last fragment
errorWrites "Error: …" into the bubble and stops speech
The message box
Think / Quick switchMicrophone: dictate one utteranceSendEmpty box: Send becomes hands-freeAnswer it now: for an unanswered question
Part VIII · The clientsChapter 35
Chapter 35

The terminal client

kreo chat is a second client, equal to the browser. It attaches to the running agent over the same /ws socket. If no agent is running, it says so and exits.

Slash commands
/mic [text]
Listen once on the agent's microphone
/voice · /text
Hands-free on or off
/tasks
Show open tasks
/reasoning
Show or hide the model's thinking
/new
Start a new conversation
/help · /quit
Help; leave (the agent keeps running)
Small differences from the browser
  • Speaks the whole reply after end (up to 2,000 characters), not sentence by sentence.
  • Never off the record.
  • Three silent turns in a row switch voice mode back to text.
  • Ignores sources and farewell.
kreo chatChapter 35
PS C:\Kreo-Agent> kreo chat Kreo · Qwen3-4B-Q4_K_M · loaded · microphone: Microphone Array Type /help for commands. you what's on my list today? thinking… 2s · intent: list_tasks kreo You have two open tasks: take the bins out at 19:30, and call the bank before 17:00. 3.1 s you remind me to stretch in 20 minutes · intent: create_reminder · task #13 kreo Done. I'll remind you to stretch at 14:50. 2.4 s you /voice voice mode on · listening on the agent's microphone

An illustrative session. Status lines overwrite themselves while the model thinks; replies are word-wrapped as they stream.

kreo chat
→
GET /api/statusis it running?
→
ws://127.0.0.1:8765/wssame as the browser
→
/api/speech/*voice on the agent
Part IX · Living on WindowsChapter 36
Chapter 36

Start with Windows, toasts and the tray

Kreo is meant to be part of Windows: it starts when you sign in, runs in the background, and taps you on the shoulder with a toast.

Start with Windows

Turning it on registers a Task Scheduler entry named Kreo-Agent, from generated XML. Never schtasks /TR, whose quoting breaks on any path with a space. Never the Run registry key.

Task Scheduler is the authority on whether it is on, not the config file. Windows drops XML elements that hold their default value, so a missing <Enabled> means enabled.

Three ways to switch it
Settings → This machinekreo autostart on|off|status [--open-client]GET/POST /api/startup
Not built yet: the tray kreo/windows/tray/ is a placeholder for the planned menu: Start, Pause, Sleep, Stop, Open Kreo, Settings, Exit. pystray and pillow are already in the optional dependencies.
WindowsChapter 36
The scheduled task, in short
<LogonTrigger>  at sign-in, current user
  <Delay>PT30S</Delay>  wait 30 s
<Principal> InteractiveToken · LeastPrivilege  no admin
<Settings>
  MultipleInstances IgnoreNew · runs on battery
  ExecutionTimeLimit PT0S          no limit
  RestartOnFailure every 1 min, 3 times
<Exec>
  <Command>.venv\Scripts\pythonw.exe  no console window
  <Arguments>-m kreo run [--open]
  <WorkingDirectory>the project folder

Written as UTF-16 to a temp file, registered with schtasks /Create /XML … /F, then the temp file is deleted.

A reminder toast
Kreo-Agent · now Reminder: Stretch Time to stand up and stretch. Open Kreo
Sent with winotify (windows/notifications/toast.py). Title capped at 64 characters, body at 200. It never raises an error; it returns whether it was delivered, and that is stored on the notification.
Part X · Every file, explainedDirectory 1
Directory · backend

kreo root and the API

Each row: the file, its line count, its job in plain words, and the key names inside it.

kreo/

__init__.py2 lines
Marks kreo as a package and holds the version, 0.1.0.__version__
__main__.py318 lines
The kreo command. Parses subcommands; only run starts the agent, the rest call its HTTP API.main · build_parser · cmd_run · cmd_stop · cmd_chat · port_is_free · call_api · find_server_pids

kreo/api/

__init__.py2
Re-exports create_app.
app.py162
Builds the FastAPI app. The lifespan creates every service at start-up and tears them down at shutdown. Serves the built client with a no-cache index.html.create_app · lifespan · FRONTEND_DIST · spa · _warm_up_listening
state.py36
The Runtime: one object holding every shared service.Runtime · AgentState · uptime_seconds
deps.py4
Hands the Runtime to route functions.get_runtime
ws.py128
The /ws WebSocket. Starts a Run for each chat message and forwards its events. Handles attach, answer, forget and ping.websocket_endpoint · _forward · _unanswered
kreo/api/routes/Directory 1

kreo/api/routes/

health.py29
Liveness check, plus which client bundle is live so old tabs can reload.health · client_build
status.py128
Overall status and the switches: model on/off, thinking on/off, start with Windows.status · model_toggle · model_suspend · model_resume · toggle_thinking · set_startup
chat.py156
Whole-reply chat, replies in progress, and the conversation archive.chat · active_runs · list_conversations · update_conversation · delete_conversation · list_messages
tasks.py115
Create, list, update, delete tasks. Keeps timestamps in step with status.list_tasks · add_task · update_task · remove_task · serialise
notifications.py52
The notification centre for delivered reminders.list_notifications · mark_read · mark_all_read
speech.py121
Speak, stop, listen and play chimes, always on the agent's machine.capabilities · speak · stop · listen · play · sound_file
location.py77
Home location, IP detection, place search, local sun times.read_location · update_location · detect · search_places · sun
dev.py242
Read-only developer views: metrics with p50/p95, logs, runtime events, audit trail.metrics · logs · events · audit · _percentile
settings.py60
Shows the effective config; changes speech and notification settings until restart.read_settings · update_speech · update_notifications
permissions.py18
View and change Auto / Ask / Never per action (memory only).list_permissions · update_permission
Part X · Every file, explainedDirectory 2
Directory · backend

The agent and its tools

kreo/core/agent/

__init__.py7
Docstring only. Re-exports nothing on purpose, to avoid a circular import.
orchestrator.py886
The brain. Classifies, checks permission, runs the tool, frames the note, streams the reply, stores everything.AgentOrchestrator · Outcome · stream · handle · parse_intent · _perform · _with_outcome · _create_task_for · _edit_task_for · _stage_conversation_delete · _answer_to_pending_delete · _apply_deletions · _search_web · _environment · _store · _audit · _metric
schemas.py108
The forms the model may fill in, and the reply object.Intent · Priority · AnnounceMode · ConversationScope · ParsedIntent · TaskExtraction · TaskAction · ConversationAction · EnvironmentQuery · AgentReply
prompts.py127
Kreo's persona with the current date and time, its honest can-do list, style rules, and the classifier prompt.SYSTEM_TEMPLATE · INTENT_PROMPT · system_prompt
runs.py176
Runs each chat turn as an agent-owned task; replays events to late subscribers.Run · RunRegistry · start · events · active · shutdown · RETAIN = 5 min
ephemeral.py76
Off-the-record threads, in memory only.EphemeralStore · TTL 2 h · MAX_MESSAGES 40 · MAX_SESSIONS 20
kreo/core/tools/Directory 2

kreo/core/tools/

__init__.py2
Docstring only. There is no tool registry.
tasks.py441
Creates and edits tasks. The model extracts fields; regexes and Python decide what is written.create_task · resolve_task · set_task_status · update_task_fields · delete_task · parse_due · parse_relative_duration · tidy_title · asks_for_written_content · asks_about_rather_than_requests
conversations.py292
Two-step deletion, yes/no and goodbye detection, and say().PendingDeletion · is_affirmative · is_negative · is_farewell · say · resolve_targets · delete_conversations · CONFIRMATION_TTL
environment.py73
Answers weather and sunrise questions with a sentence of real figures.answer · parse_day · EARLIEST_DAY · FURTHEST_FORECAST_DAYS
weather.py159
Open-Meteo forecast and archive, turned into one factual sentence.for_day · describe · WeatherReport · WMO_CODES
astronomy.py143
Offline sunrise, sunset, solar noon and twilight (NOAA).sun_times · describe_sun · SunTimes
location.py135
Named place, then home setting, then opt-in IP lookup.resolve · geocode · locate_by_ip · Place
web.py218
DuckDuckGo search, safe page fetching, numbered sources.research · search · fetch · format_for_prompt · is_public_http_url · ANSWER_INSTRUCTIONS
Part X · Every file, explainedDirectory 3
Directory · backend

Services and speech

kreo/core/ (other services)

permissions/engine.py44
Looks up an action's level: Auto, Ask or Never. Unknown actions are Ask.PermissionEngine · PermissionDecision · evaluate · set_default
scheduler/service.py238
APScheduler loop: due reminders every 20 s, pruning every 30 min. Never calls the model.SchedulerService · check_due_tasks · _deliver · _announce · _in_quiet_hours · prune_finished_tasks
resources/state.py123
A snapshot of CPU, RAM, GPU, VRAM, foreground app and battery. The watching loop is Stage 7.snapshot · SystemSnapshot · ResourceState
memory/__init__.py1
Placeholder for the memory manager (Stage 3).
integrations/__init__.py2
Placeholder for email, calendar and Slack (Stages 8–10).

kreo/core/speech/

__init__.py11
Public surface: speak, play_sound, list_voices.
tts.py224
Decides how Kreo speaks: Piper first, Windows SAPI as fallback. Handles stop mid-word.speak · silence · speak_neural_blocking · speak_blocking · pick_voice · PERSONA_VOICES
piper_engine.py121
The neural voice: loads Piper once, renders text to WAV.synthesise · SPEAKERS · resolve_speaker · is_installed
stt.py199
Records from the agent's mic until you stop, then faster-whisper transcribes.listen · record_utterance · transcribe · warm_up · Heard
sounds.py76
Plays the bundled chimes and WAV files with winsound.play_sound · play_wav · stop_playback · SOUND_NAMES
kreo/llm/Directory 3

kreo/llm/

models/__init__.py28
A catalogue of candidate models (Qwen3-4B, Qwen3-1.7B). Data only; not used at runtime.ModelCandidate · CANDIDATES
providers/base.py90
The contract every model backend follows.LLMProvider · ChatMessage · StreamDelta · GenerationResult · ModelInfo
providers/__init__.py16
Re-exports the base types and the mock, without importing heavy backends.
providers/llama_server.py367
The default backend: starts llama-server.exe and talks to it over HTTP. JSON-schema output, streaming, empty-reply retry.LlamaServerProvider · load · stream · generate · structured_generate · _command · _kill_stale_servers
providers/llama_cpp.py191
In-process backend using llama-cpp-python. Do not use on this machine.LlamaCppProvider
providers/mock.py78
Predictable fake backend for tests and --no-model.MockProvider
runtime/manager.py150
Builds the chosen provider and manages load, unload, suspend and resume.ModelManager · ModelState · build_provider · toggle · status
runtime/jobobject.py98
A Windows job object, so llama-server dies with Kreo.KillOnCloseJob · JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
runtime/cuda.py51
Makes pip-installed NVIDIA DLLs findable. No GPU detection.cuda_dll_dirs · register_cuda_dlls

Outside git

bin/llama.cpp/
llama-server.exe, ggml-cuda.dll (about 547 MB), CPU kernel DLLs for many chip families. The build picks one at runtime.
models/
Qwen3-4B-Q4_K_M.gguf (about 2.5 GB), piper/en_US-l2arctic-medium.onnx (about 77 MB). Whisper is downloaded and cached by faster-whisper.
Part X · Every file, explainedDirectory 4
Directory · backend

Storage, config and the CLI

kreo/database/

engine.py154
Opens async SQLite, creates tables and search indexes, adds new columns.Database · init · session · _add_missing_columns · get_database
models.py266
All 21 tables as SQLAlchemy classes, plus UTC helpers.Conversation · Message · Task · Notification · AuditLog · GenerationMetric · ResourceEvent · LogRecord · utcnow · iso_utc …
log_sink.py114
Copies log lines into log_records in batches every second.LogSink · BufferingLogHandler
queries.py12
Turns GROUP BY rows into a {label: count} dict.counts_by

kreo/config/

settings.py263
Every setting as typed Pydantic models; loads defaults, the JSON file and env variables; the built-in permission table.KreoSettings · load_settings · get_settings · _builtin_permissions · PermissionLevel · DEFAULT_KREO_PORT
logging.py34
One-time logging set-up: console and rotating file.setup_logging

kreo/cli/

chat.py410
The terminal chat client, attached over /ws, with voice through the agent.run_chat · Session · Stream · Printer · _send · _listen_once · _speak
Windows, scripts and root filesDirectory 4

kreo/windows/

notifications/toast.py40
Windows toasts through winotify. Never raises; reports delivery.send · ToastResult · APP_ID
startup/task_scheduler.py212
Registers, reads and removes the logon task from generated XML.enable · disable · status · build_xml · parse_status_xml · run_now · TASK_NAME
tray/__init__.py1
Placeholder for the tray menu.

scripts/

kreo.cmd · kreo.ps113 · 14
Launchers that run the venv's Python with -m kreo.
install-path.ps153
Adds scripts\ to the user PATH (not setx, which truncates at 1,024 characters). -Remove undoes it.
make_sounds.py112
Synthesises the marimba, chime and ping WAVs from decaying sine waves. Original, no licence needed.

Project root

pyproject.toml
Package kreo-agent 0.1.0 by MTS, Python ≥ 3.11. Console script kreo. Extras: speech, windows, gpu, dev, and legacy llm.
config/kreo.example.json
The config template, and the fallback when kreo.json is missing.
benchmarks/run.py 118 · tasks.py 103
Stage 0 benchmark that chose the model. Results saved as JSON in benchmarks/results/.
CLAUDE.md
Working notes: conventions, rules and hard-won traps.
initial-docs/
PRD, TRD, the 15-stage plan and the design brief.
Part X · Every file, explainedDirectory 5
Directory · frontend

The web client's core files

frontend/src/

main.tsx13
Entry point: loads the font and tokens, renders App in a router.
App.tsx115
Stacks the providers, builds the frame, declares routes, Ctrl/Cmd+K.App · Shell · Pages · Root
api.ts449
Every REST call, the data types, WebSocket event types, and British 24-hour formatters.api · openSocket · WsEvent · fmtTime · fmtDateTime · fmtDuration · loadedBundle
ChatContext.tsx681
The conversation: messages, socket, voice, dictation, off the record. Sits above the router.ChatProvider · useChat · Bubble
StatusContext.tsx83
Polls /api/status every 4 s; model and thinking switches; reloads a stale bundle.StatusProvider · useStatus
useVoiceLoop.ts214
The hands-free loop and its one listening rule; owns the SpeechQueue.useVoiceLoop · ChatMode
speech.ts246
Asks the agent to speak sentence by sentence; browser dictation fallback.SpeechQueue · AgentSpeaker · forSpeech · speakOnce · silenceAgent · startDictation
navigation.tsx128
The sidebar menu as data: groups, items, icons, badges.NAVIGATION · navItemFor
theme.tsx66
Light, dark or system, saved in localStorage.ThemeProvider · useTheme
useNow.ts15
A clock that ticks every 30 s so "overdue" labels update.
index.css · App.css216 · 8
Design tokens for both themes; App.css imports the four style sheets.
styles/*.cssshell 500 · ui 1,094 · chat 888 · pages 677
Frame and sidebar; shared pieces; the chat screen; page layouts.
frontend/src/components/Directory 5

frontend/src/components/

Sidebar.tsx98
Left menu with state dot, badges, live chat dot, theme toggle, host and uptime.
TopBar.tsx54
Page title, search, bell, Model switch, error banner.
CommandMenu.tsx144
Ctrl+K search over pages, tasks and conversations.
ConfirmDialog.tsx111
In-app confirmation that replaces window.confirm.
ErrorBoundary.tsx50
"This page could not be displayed" instead of a blank window.
NotificationBell.tsx87
Unread count; list and mark read on open.
LiveVoice.tsx87
The full-screen hands-free overlay.
KreoOrb.tsx · KreoMark.tsx41 · 61
The animated state orb; the still logo with its own gradients.
Markdown.tsx60
Renders replies; no raw HTML; closes half-written code blocks mid-stream.
ThinkingBlock.tsx15
Folded view of the model's reasoning.
Sources.tsx84
Numbered source chips with local letter icons.
LineChart.tsx185
SVG line chart with tooltip and a table view.
StatTile · Meter · Badge · StatusDot37 · 38 · 20 · 9
Figure tile; percentage bar (amber 75 %, red 90 %); coloured badge; state light.
Toggle · ThemeToggle39 · 49
On/off switch that also says its state; light/dark/system buttons.
EmptyState · PageHeader22 · 19
"Nothing here yet" panel; page title block.
Part X · Every file, explainedDirectory 6
Directory · frontend

Pages

frontend/src/pages/

OverviewPage.tsx378
The dashboard. Polls tasks, metrics and conversations every 15 s.
ChatPage.tsx484
The conversation screen; all requests go through ChatContext.
TasksPage.tsx192
Task list with filters and status buttons. Refreshes every 10 s.
ActivityPage.tsx182
Merged timeline of audit, events and reminders. Refreshes every 5 s.
AutomationsPage.tsx164
Scheduler jobs and waiting reminders.
MemoryPage.tsx129
Placeholder with counts and stored conversations.
IntegrationsPage.tsx132
What is connected, what is planned.
AnalyticsPage.tsx223
Speed and latency charts over 1 h to 30 d.
PermissionsPage.tsx161
Auto / Ask / Never per action, recent audit.
DevPage.tsx319
Transcripts, logs, runtime events.
SettingsPage.tsx566
Appearance, this machine, voice and sound.
tests/Directory 6
Tests: 32 files, 236 test functions

They use the mock provider and a temporary SQLite file, so they never need a model. conftest.py turns notifications off: tests must never raise a toast, play a sound or speak.

Agent and chat

chat_runscontext_budgetreasoning_budgetwritten_contentfarewelloff_the_recordresilience

Tools

taskstask_editingrelative_timeenvironmentwebconversationsdelete_conversation

Voice and model

voicevoice_personaneural_voicelocal_speechannouncementsmodel_controls

Storage and config

migrationstimestampstitles_and_pruningconfigpermissionspermission_defaults

API, clients, Windows

apidevcliclient_buildschedulerautostart

Each chip is a file named tests/test_<chip>.py. Groups are by file name.

Part XI · ReferenceReference 1
Reference

Every endpoint on port 8765

All HTTP routes live under /api. Anything else serves the web client.

MethodPathPurpose
GET/api/healthAlive, version, client bundle
GET/api/statusState, uptime, model, resources, scheduler, counts
GET/api/status/modelModel status only
POST/api/model/toggleSuspend or resume the model
POST/api/model/suspend · resumeFree the VRAM, or bring the model back
POST/api/model/load · unloadLoad or unload directly
POST/api/model/thinking · /toggleReasoning on or off, for the next message
GET / POST/api/startupStart with Windows, read from Task Scheduler
POST/api/chatWhole-reply chat (not used by clients)
GET/api/chat/activeReplies still being written
GET/api/conversationsList, with q, include_archived, limit
PATCH / DELETE/api/conversations/{id}Rename, archive, or delete
GET/api/conversations/{id}/messagesMessages with reasoning and figures
GET / POST/api/tasksList (status, kind) or create
PATCH / DELETE/api/tasks/{id}Change or delete a task
Endpoints, continuedReference 1
MethodPathPurpose
GET/api/notificationsList, unread_only
POST/api/notifications/{id}/read · read-allMark read
GET/api/speech/capabilitiesVoices, microphone, sounds, settings
POST/api/speech/speakSpeak on the agent; returns when finished
POST/api/speech/stopStop speaking, clear the queue
POST/api/speech/listenRecord and transcribe one utterance
POST · GET/api/speech/play/{name} · sounds/{name}.wavPlay a chime on the agent; serve it for preview
GET / PATCH/api/locationHome location (this run only)
POST · GET/api/location/detect · search?q=IP lookup; geocode a place
GET/api/location/sun?day=Sun times, computed locally
GET/api/dev/metrics · logs · events · auditDeveloper views
GET/api/settingsThe effective config
PATCH/api/settings/speech · notificationsChange until restart
GET · PUT/api/permissions · /{action}Read or set a level
WS/wsChat streaming (chapters 11 and 16)
GET/assets/* · /{any}Client files; anything else returns index.html
Part XI · ReferenceReference 2
Roadmap

Where the project stands

Kreo is built stage by stage from initial-docs/stages.md. Each stage must leave a system that runs.

Built and working today
  • Chat with a local Qwen3-4B, streaming, with visible reasoning
  • Tasks and reminders created, edited, completed and deleted by conversation
  • Reminders as toasts, chimes and spoken announcements, with quiet hours
  • Web search with citations; offline sun times; Open-Meteo weather
  • Voice in and out on the agent's machine, and a hands-free mode
  • Off-the-record chats; safe two-step conversation deletion
  • Permissions with an audit trail; start with Windows
  • A web client with eleven screens, and a terminal client
Next up
Memory extraction (Stage 3)Open-loop monitoring (4)Tray app (6)Automatic GPU sleep (7)Approval screen (12)
StagesReference 2
DonePartly builtPlanned
STAGE 0Architecture & benchmarkModel chosen: Qwen3-4B
STAGE 1Kreo coreFastAPI, SQLite, orchestrator
STAGE 2Local web clientReact, WebSocket streaming
STAGE 3MemoryConversations stored, FTS5; no extraction yet
STAGE 4Tasks & open loopsTasks, reminders; no monitoring
STAGE 5SchedulerAPScheduler reminders
STAGE 6Windows integrationAutostart, toasts; no tray
STAGE 7Resource-aware runtimeSnapshot, manual suspend
STAGE 8Email
STAGE 9Calendar
STAGE 10Slack
STAGE 11Unified dashboardOverview page
STAGE 12Permissions & autonomyLevels, audit; no approval UI
STAGE 13Agent identity
STAGE 14Cross-device
STAGE 15Advanced autonomy
Part XI · ReferenceReference 3
Field notes

Surprises in the code

Things that differ from what the comments say, or that catch newcomers out. Worth knowing before you change anything nearby.

ConfigKREO_* variables only fill keys missing from kreo.json, although the docstring says they win.
Nothing is saved backSettings, location, permission and autostart changes made while running live in memory only.
--reloadkreo run --reload ignores --config and --no-model: uvicorn calls the factory with no arguments.
Logs twiceConsole lines may print twice: main() calls basicConfig, then setup_logging adds a second handler.
Field notesReference 3
Out-of-date docstringws.py leaves out the sources and conversation_deleted events, and several fields.
Where the empty-reply retry livesprovider.stream() has none; orchestrator.stream() does it. provider.generate() has its own.
Two classes called ModelStateA database table in database/models.py and an enum in llm/runtime/manager.py.
PlaceholdersTray, memory, integrations and the resource watcher are stubs. /api/status always reports ACTIVE.
Unused on purpose, for nowClient helpers api.health, api.chat, api.stopSpeaking and browserSpeechSupported are never called. The model catalogue CANDIDATES is not read at runtime.
Pending deletes forget on restartA "Shall I delete…?" waiting for your answer is held in memory for 5 minutes. A restart drops it, which errs on the safe side.
CSS class collisionsA layout class named .live once matched a StatusDot modifier and drew big green blobs on four screens. Layout classes now get names nothing else would use, like .voice-stage.
Part XI · ReferenceReference 4
Glossary

Words you will meet

Agent
A program that runs by itself and acts for you. Here, the Python process started by kreo run.
LLM
Large language model: the AI that reads and writes text. Kreo uses Qwen3-4B.
GGUF
The file format llama.cpp loads models from.
Quantised (Q4_K_M)
Numbers stored in about 4 bits instead of 16, so the model fits a small GPU.
Token
A piece of a word, about 3–4 characters. Budgets are counted in tokens.
Context window
How many tokens the model can see at once: 4,096.
Reasoning / thinking
Private notes the model writes before answering.
Structured output
Forcing the model to reply with JSON that fits a schema.
Intent
The kind of request: chat, create a reminder, weather…
Orchestrator
The code that runs a chat turn from start to finish.
Note
Facts or a sentence Python hands the model to phrase.
Run
One chat turn, owned by the agent, with all its events.
VRAM
Memory on the graphics card. The GTX 1650 has 4 GB.
GlossaryReference 4
FastAPI
The Python web framework behind /api and /ws.
Lifespan
FastAPI's start-up and shutdown hook, where services are built.
WebSocket
A two-way connection that stays open, used to stream replies.
SSE
Server-sent events. llama-server streams to Kreo this way.
SQLite
A database in a single file: data/kreo.db.
WAL
Write-ahead logging: lets SQLite read while it writes.
FTS5
SQLite's full-text search index.
ORM
Python classes that stand for tables (SQLAlchemy).
Job object
A Windows group of processes that can be killed together.
TTS / STT
Text to speech (Piper) and speech to text (faster-whisper).
SAPI
Windows' built-in speech voices, Kreo's fallback.
APScheduler
The Python library that runs jobs on a timer.
Toast
A Windows notification in the corner of the screen.
keyring
Stores secrets in Windows Credential Manager.
Audit log
A permanent record of sensitive actions.
Part XI · ReferenceBack matter
Back matter

Running it yourself

The commands a developer uses every day, from the project root in PowerShell.

# run the tests (mock model, temporary database)
.\.venv\Scripts\python.exe -m pytest

# lint and format
.\.venv\Scripts\python.exe -m ruff check kreo tests benchmarks
.\.venv\Scripts\python.exe -m ruff format kreo tests benchmarks

# start the agent without the AI model
.\.venv\Scripts\python.exe -m kreo --no-model

# chat from the terminal
.\.venv\Scripts\python.exe -m kreo chat

# rebuild the web client
cd frontend; npm run build
Before changing the architecture Read initial-docs/prd.md, trd.md and stages.md, and the rules in CLAUDE.md. Each stage must leave a runnable system.

Kreo runs on
your system.

The browser is only one way in. Every capability works with no browser open.

Inside Kreo · edition of 26 September 2026 · MTS