Skip to content
FosowlPublic

About

Extremely fast & token-frugal agentic framework for small or hardware constrained LLMs. Run agents in a cave with a box of scrap. 3 Tokens per action or bust!.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

1 watching

Forks

Repository files navigation

ThreeToks Logo

License: MIT Python 3.10 or newer Runs on your own computer The core needs no extra packages

See it work · Quickstart · Agents · Config · Models · Design · Contribute


ThreeToks

ThreeToks is a small AI agent that runs on your own computer. It works with small language models. Small models are fast and use little memory. But they make mistakes when they write long text.

So ThreeToks does not ask the model to write. It asks the model to choose.

Think of a ship. The code is the crew. It reads the charts, keeps the log, and steers. The model is the lookout. At each fork in the sea, it only says one thing: "This way."

A typical agent ThreeToks
What the model does at each step Writes a long plan Picks one number from a menu
Tokens used per step about 200 – 500 about 1 – 3
Time per step about 15 – 30 seconds about 0.3 seconds

A token is a small piece of text, about three quarters of a word. Fewer tokens means a faster, cheaper step.

Our goals

  • Main goal. Make an agent that is useful even with a 1.5b model on a computer with less than 4 GB of memory. Each decision step must take less than one second.
  • Second goal (in progress). Work with Colibri. Colibri runs the large glm-5.2 model from a fast SSD with less than 16 GB of RAM. See Colibri.

How it works

  1. The code builds a tree of decisions. Each decision is a numbered menu.
  2. The model answers with one number. This takes about 3 tokens.
  3. The code does all the exact work: it reads pages, saves notes, and can undo a step.
  4. To take notes, the model points at numbered sentences. It never rewrites them. So the code never invents text for a note.
  5. A judge checks the final answer. If the answer is weak, the agent searches again.

The model only makes choices. Everything that must be exact is normal code.

See it work

Animation of the question: Why do leaves change color in autumn? A glowing path moves down a tree of choices from route to web to search results to a page. Numbered sentences become notes. A weak answer is rejected. The agent searches again, and three judges approve the new answer.

Click the picture to open the full video (1080p, 60 fps, no sound, 19 seconds).

Still image · Editable Manim source

The video asks a simple question: "Why do leaves change color in autumn?" You can see these steps:

  • A path moves through the tree and routes the question to the web agent.
  • The agent opens a search result.
  • It copies numbered sentences into notes that keep their source.
  • A judge rejects a weak answer.
  • The agent searches again with a new angle and the same notes.
  • Three judges approve the new answer.

Please note. The video is a script. It is not a live run, and it does not show a measured result. The menus and page text are shorter than real ones. The graph shows the chosen branches, not every branch. The judge comments are labels that we wrote. The model did not generate them. Real runs can end with no approved answer, for example when no source is available. The video leaves out recall, link-following, note curation, and error recovery. This keeps it short.

Render the video again

You only need this to change the video. You do not need it to run ThreeToks.

  1. Install Manim Community 0.19.2 in a separate Python environment.
  2. Run python3 docs/animations/decision_tree.py. This writes docs/animations/decision_tree.mp4. Temporary files stay in the ignored docs/animations/.render/ folder.
  3. Run python3 docs/animations/make_preview.py. This makes the small GIF that this page shows. GitHub cannot play an MP4 file inside a page.

The scene uses the Avenir Next font when it is available. Other systems use the font that Manim picks.

Quickstart

You need three things: a model server, ThreeToks, and one command.

1. Get a model. Install Ollama. Then download the default model:

ollama pull qwen3.5:4b

Other local servers also work. See Choose a model.

2. Install ThreeToks. You need Python 3.10 or newer and uv.

git clone https://github.com/Fosowl/3toks.git
cd 3toks
uv tool install --editable ".[web]"

The web part adds web search. You can add more features. See Optional features.

3. Start it.

threetoks

The first start opens the config screen and saves your settings. After that, you can chat.

You can also ask one question and exit:

python3 -m threetoks.cli "When was the Eiffel Tower completed?" --rounds 3

Two optional boosters

Both make web research better.

  • SearXNG is a private search engine that runs on your computer. Start it with ./start_search_engine.sh. See infra/searxng/README.md. ThreeToks finds it by itself. Without it, ThreeToks searches Bing, then DuckDuckGo, and needs no setup. The Chrome mode below needs SearXNG.
  • Chrome mode opens pages in a real Chrome window. Use it for pages that need JavaScript. Type /browser plain in the chat, or set [browser] mode = plain in the config. Use /browser plain headless to hide the window. Use /browser http to go back to the simple mode. This mode needs Chrome and the browser feature.
More about Chrome mode
  • Chrome hides its automation switches. It keeps one profile in ~/.cache/threetoks/chrome-profile. So cookies and solved bot checks stay between sessions.
  • A site can show a bot check. A visible window then waits 45 seconds, so that you can solve it. A site that stays blocked is skipped until the session ends.
  • plain mode works together with SearXNG. If no SearXNG answers, ThreeToks refuses to start in this mode and tells you to run ./start_search_engine.sh.

Optional features

The core of ThreeToks needs no extra package. Each feature below is an add-on. Install an add-on to get the feature. Skip it, and ThreeToks still starts. It just leaves that feature out.

Add-on What you get Packages it installs
web The web research agent curl_cffi, beautifulsoup4, markdownify
browser Chrome page fetching (/browser) selenium
stt Voice input: speak, and it types vosk, sounddevice
tts Voice output: it speaks the answer piper-tts, sounddevice, numpy
voice Both stt and tts —
camera The look agent (webcam and a vision model) opencv-python
relay The light agent (a lamp on a Raspberry Pi only) RPi.GPIO (ARM only)

Pick the add-ons when you install:

uv tool install --editable ".[web,browser]"      # research
uv tool install --editable ".[web,voice,camera]" # voice assistant
uv tool install --editable ".[web,voice,relay]"  # Raspberry Pi

To work on the code itself, install into the project's own environment instead:

uv pip install -e ".[web,browser]"

The config screen

Type /config in the chat, or run threetoks config in a terminal. A control panel opens on the terminal's second screen. Your chat comes back when you leave.

Ten sections, named like parts of a harbor, hold every setting:

Section What it controls
HELM The model and how to reach it
NETS How web pages are fetched
SONAR Where searches go
DEPTH How deep the research goes
DOCK Where your files live
LOGBOOK What it remembers
FORGE The code agent
COMMS Talk and listen
SCOPE The periscope camera
BEACON The lighthouse relay
Key Action
↑ ↓ or j k Move between settings
← → or h l or Space Change a choice, a switch, or a number. Also steps through your Ollama models
Enter Flip a switch, or open a text box for a text or path setting
Tab / Shift-Tab, 1–9, 0 Go to the next or previous section, or jump to one
r Put the setting back to its default
s Save
Esc Leave. Press it twice to drop changes you did not save

Every change works in the running chat, with no restart. The chat reads the config file again before it handles your next line, so a save from the screen, from threetoks config set, or from your editor all apply the same way. The chat prints the settings it applied. If the file has an error, the chat keeps its current settings and tells you the error. If one part cannot start (for example Chrome or a voice model), the chat says so, keeps the old value for that part, and tries again at the next save.

A small symbol at the end of a row is a live check. It tells you that Ollama answers, that the model is downloaded, that the API key is set, that SearXNG answers, or that a path exists.

A value that ThreeToks cannot use (for example vote_k = 0 or min_interval_s = soon) falls back to its default. An unknown name (for example [LLM] or votek) is ignored. The chat, the screen, config show, config set, and doctor name each of these. The next save writes the default in place of a value that cannot be used. A comment can follow a value after a space: vote_k = 3 # three votes. ~ in a path setting means your home folder.

When you save, ThreeToks changes only the lines you edited (and the values it named as unusable). Your own comments and the order of your keys stay as they are. Without a real terminal (for example in a pipe, or with TERM=dumb), /setup asks the same questions one line at a time.

Where ThreeToks looks for the config file, in this order:

  1. The path in $THREETOKS_CONFIG.
  2. ./config.ini in the current folder.
  3. Your own file: ~/.config/threetoks/config.ini on Linux and macOS ($XDG_CONFIG_HOME works too), or %APPDATA%\threetoks\config.ini on Windows.

Command line

threetoks                         # chat
threetoks -m qwen3.6:27b          # chat with another model, this run only
threetoks -c ./other.ini          # use another config file
threetoks --no-color              # plain text (NO_COLOR also works)
threetoks config                  # open the config screen
threetoks config show             # show every setting; pink means "not the default"
threetoks config get llm.model
threetoks config set browser.mode plain
threetoks config path
threetoks doctor                  # check Ollama, model, keys, SearXNG, paths, add-ons
threetoks --version

Put the flags -m, -c, and --no-color before the subcommand. config set checks your value like the screen does. It exits with code 2 for a bad key, a bad value, or a missing value. It exits with code 1 when the file has an error or cannot be written; it does not change a file that has an error. doctor exits with code 1 when a check fails or a value is ignored, so you can use it in scripts.

Agents

Every request goes through a router. The router shows the model a menu of agent names. Each name has a one-line description. The model picks one digit. A small model can pick a different agent when only the order of the menu changes. So the router asks twice, with two different orders. If both answers agree, that agent runs. If not, it asks again (5 times at most) and takes the majority. If no answer is valid, ThreeToks uses the first agent in the list, casual. Questions about the code in the current project ("what does this repo do?", "where is the config loaded?") go to code. General questions about a language or tool ("how do decorators work?") go to web.

A request with several steps goes to the planner. Example: "find the population of Japan, then write a script that prints it".

Agent What it does Details
casual Small talk and quick replies It ranks ready-made replies by shared keywords. An exact match needs no model call. If nothing fits, it writes a short free reply.
web Answers questions with an internet search It runs the deep research loop: search, read pages, follow links, take notes.
files Answers questions about your files and folders It stays inside services.files_root. It can run one short shell command, but only after a fixed block-list and a fresh safety judge both allow it.
code "Where is X?", "Fix the bug in Y", "Work out Z from data.csv", "Write a script that…", "Install numpy" Five modes. See the list below.
light "Turn on the light" Needs a Raspberry Pi and the relay add-on. Clear phrases are matched by simple rules, with no model call. Other phrases cost one menu decision.
look "What am I holding?" Needs the camera add-on and a vision model (llava, qwen-vl, gpt-4o, claude, gemini, …). It takes one frame and makes one short vision decision.
planner "Find X, then write code with it" It writes a plan, called a trajectory: the agents in order, each with a very short task. See below.

The code agent has five modes:

  • Navigate finds code. It is free when your request names the symbol. Otherwise it shows a ranked short list.
  • Edit changes one function. It shows a ranked list, then a menu with the function's source. The model writes only the changed function. Your repo's tests, or an import check, judge it. You then see a diff. Nothing is written until you press y.
  • Compute runs a script once in [files] root and gives you the result.
  • Author plans small functions and writes them one by one, plus a main() that does the task. Then it runs the script once in [files] root with your project's Python (.venv there, else python3). Under the code you see the exit code, the time, and what the script printed. Files the script saves land in that folder. A script that crashes is marked UNVERIFIED.
  • Shell writes one terminal command: install a package, run the tests or a file, use git, move or delete files. You see the command first. It runs only after you press y, in [files] root, with your shell. PYTHON, python and pip in the command become your project's Python.

The functions that the agent writes are checked in a temporary folder with no API keys in the environment. The final author run and shell commands are not sandboxed: they can change any file you can change.

The planner works in three parts:

  1. Plan. The model sees the other agents, each with a number. It gives the numbers in order, for example 2,4. An agent can come back. Then it writes one very short task for each step.
  2. Run. Each agent does its step. Its task is only its own short task. Every model call of the step also sees the overall goal and the answers of the steps before it.
  3. Correct. After each step, the model says yes or no: must the next steps change? On yes, it plans the remaining steps again.

A plan has 4 steps or fewer. A request runs 6 steps at most, and the plan changes 2 times at most. The progress tree shows the plan, and each step is one branch of the tree. If the plan is not valid, the router picks one agent for the full request. When a plan has 2 or more steps, the final answer shows the answer of each step under its number. The planner is always the last agent in /agents. It is added only when 2 or more other agents are ready.

The progress tree

While a request runs, the chat shows each model decision as one row of a tree:

  ◆ pick an agent
  ├ ▂ choose  web — look up facts or current informat…           2t·0.28s
  ╰ web · 1 call · 2t · 0.3s
  ◆ web agent
  ├ ◆ round 1 · search and read
  │ ├ ▂ choose  open result 5: RFI - All the news from …         2t·0.92s
  │ │ ├ ▃ pick    24                                             3t·0.83s
  │ │ │   · The soundtrack to France’s school protests
  │ ├ ▂ choose  answer the task now                              2t·0.33s
  │ │ ├ █ write   is 2025, and high school protests conti…      15t·0.51s
  │ ╰ done · 4 calls · 22t · 6.9s
  ├ ◆ check the answer
  │ ├ ▂ judge   bad — vague, off-topic, or does not ans…         2t·0.41s
  │ ╰ rejected · 1 call · 2t · 0.4s
  ╰ answered · 5 calls · 24t · 7.3s
  • A ◆ row starts one part of the request: choosing the agent, recalling past answers, the agent itself, each research round, the answer check, the choice of the next search, and each planner step.
  • A ╰ row ends that part. It gives the result (the agent, the verdict, the new search), the number of model calls, the tokens, and the time.
  • A choose row is an action that the model picked from a menu. The rows under it are the decisions that this action caused, for example the sentences the model picked from an opened page.
  • The bar at the start of a row is the token cost of the decision (▁ = 1 token, █ = 8 or more). Its color is the speed: green is fast, amber is steady, pink is slow.

Chat commands

Command What it does
/help Show help
/agents List the agents that are ready
/deep N Set the number of research rounds
/browser http|plain [on-screen|headless] Choose how pages are fetched
/model NAME Change the model
/voice on|off|debug Talk instead of typing. debug shows what the microphone hears, and why each gate dropped it
/debug [on|off|full] Show the raw model answer and the reason it stopped. full also shows the whole prompt of each model call
/config Open the config screen (/setup does the same)
/quit Leave

On a real terminal, the prompt remembers your history between sessions (~/.config/threetoks/history). You can edit a line with the arrow keys. Tab completes commands and their arguments: /de<Tab> gives /deep, and /debug f<Tab> gives full.

Want a new agent? It is one new module and one line in the registry. See docs/AGENTS.md.

Choose a model

ThreeToks is local first. You need no account and no API key. Nothing leaves your computer.

Provider Set provider = Mode
Ollama (default) ollama raw. This is the path we measured.
LM Studio lm-studio chat, at localhost:1234
Any local server with an OpenAI-style API (llama.cpp, vLLM, Colibri, …) custom, plus api_base chat
[llm]
provider = custom
api_base = http://127.0.0.1:8080/v1
model = my-local-model

If your local server needs a key, put it in the LLM_API_KEY environment variable. Otherwise you need no setup. ThreeToks has no SDK to install.

Thinking models. [llm] disable_think = true is the default. It starts each raw answer with an empty <think></think> block. Thinking Qwen models need this. Without it, qwen3.8 answers every menu with nothing, and the router falls back to casual. Set it to false to send the plain prompt.

Debugging. /debug on shows each raw answer and the reason it stopped. The reason length means that the node's token limit cut the reply before the model answered.

Cloud providers (optional)

You never need a cloud API. Everything above runs on your computer. A cloud API is useful to try ThreeToks before you download a model, or to see how a larger model handles the same tree. Set [llm] provider, then export the provider's API key:

Provider Set provider = API key variable
Anthropic anthropic ANTHROPIC_API_KEY
OpenAI openai OPENAI_API_KEY
OpenRouter openrouter OPENROUTER_API_KEY
Together together TOGETHER_API_KEY
DeepSeek deepseek DEEPSEEK_API_KEY
Google (Gemini) google GOOGLE_API_KEY
[llm]
provider = openrouter
model = qwen/qwen-2.5-7b-instruct

ThreeToks reads keys from the environment only. It never reads them from a config file. Cloud providers use the same chat mode as the other chat providers in the table above. They work, but we do not recommend them.

Colibri (work in progress)

Colibri is an engine in the style of llama.cpp. It runs GLM-5.2 (744B parameters, int4) by streaming parts of the model from an SSD. ThreeToks needs no new code for it. coli serve speaks the OpenAI chat API, so Colibri plugs in as a custom provider:

[llm]
provider = custom
api_base = http://127.0.0.1:8000/v1
model = glm-5.2     ; must match the --model-id of coli serve
send_seed = false   ; Colibri rejects the seed parameter

The two fit each other well:

  • ThreeToks prompts only grow at the end. Colibri re-uses the start of the prompt in each slot. So each step reads only the new part, which is the slow part on an SSD engine.
  • ThreeToks answers are 3 to 24 tokens. That is the easiest work for a decoder that makes 0.05 to 7 tokens per second.
  • Colibri turns thinking off by default, reports token use, and makes one answer at a time. ThreeToks asks for one answer at a time too.

Status: partly working. Colibri rejects two standard request parameters with an HTTP 400 error:

  • seed — solved on our side. send_seed = false leaves it out. The only cost is that the third retry (temperature 0.4) is no longer repeatable. All other tries use temperature 0, where a seed changes nothing. With this setting, menu decisions work from start to end against the Colibri server.
  • stop — still blocks. Pick-many and short-text nodes send a "\n" stop sequence. Colibri answers: "Custom stop sequences are not supported yet." Every vertical uses short-text nodes, so full runs need stop support in Colibri. Cutting the text on the server side is enough, because our answers are only 3 to 24 tokens. A pull request is coming. (I just bought a new SSD, so that I can run Colibri and test it well.)
Limits that remain after it connects
  • It runs in the same reduced chat mode as every other chat provider (see Choose a model). The prefill is a hint, not a fixed start. So a single-digit answer depends on how well GLM-5.2 follows instructions. A later option is the /v1/completions endpoint of Colibri (a raw prompt with no template), plus a GLM chat template in our family table.
  • The HTTP timeout is fixed at 120 seconds. Colibri sends nothing until a non-streaming answer is complete. On a slow SSD, one decision can take longer. A timeout you can set is on the list.
  • Colibri makes 0.05 to 7 tokens per second, depending on the hardware. A ThreeToks run spends only a few tokens per step, which is the point. But a run with many steps still takes minutes, not seconds. Sub-second steps stay a property of a local Ollama model.

Voice, camera, and relay

Each feature turns on when you install its add-on. You do not change any code. The settings live in the [voice], [camera], and [relay] sections of config.ini. The setup wizard writes them with commented defaults.

Voice input — add-on stt (vosk, sounddevice)

Type /voice on to listen on the microphone. Each sentence passes three gates before an agent sees it:

  1. A junk list removes the nonsense that Vosk invents from silence.
  2. An echo filter compares the text with the last spoken answers. So the assistant never talks to itself.
  3. A one-token menu asks the model: "meant for the assistant" or "background noise". This costs 1 token for each sentence that passes the free gates.

The Vosk model downloads by itself for [voice] lang (default en-us). Set stt_model_path to use a local model.

/voice debug shows each raw transcript, why a gate dropped it (empty, junk, or echo), and whether the menu judged it as meant for you or as noise. If nothing shows up while you speak, check the microphone permission of your terminal in your operating system.

Voice output — add-on tts (piper-tts, sounddevice, numpy)

ThreeToks speaks answers with a local Piper voice. It does not download voice models for you. Get a .onnx file and its .onnx.json file from rhasspy/piper-voices. Keep them side by side. Then point [voice] tts_model_path at the .onnx file.

  • The microphone is fully muted while ThreeToks speaks. It stays muted for post_speak_delay seconds more, so the echo can fade.
  • The add-on voice installs both sides. Each side also works alone: voice in with text out, or text in with voice out.
  • Set [voice] enabled = true to start the chat already listening.
  • /setup has an optional voice step. It explains that you need both the .onnx file and its .onnx.json file, offers to open the Vosk and Piper pages, and asks again until a path works (or you keep your own).
  • A saved [voice] change restarts a live voice session with the new settings. Switching enabled on or off starts or stops voice at once.
Camera — add-on camera (opencv-python)

The add-on registers the look agent when your model can see images. One JPEG frame from the device [camera] index goes with one short decision. An image costs many more tokens than a menu decision. So choose a vision model on purpose, for example ollama pull llava and then /model llava.

Relay — add-on relay (RPi.GPIO, ARM only)

On a Raspberry Pi, the add-on registers the light agent. It drives an active-low relay board on BCM pin [relay] pin (default 17). On any other computer, the add-on installs nothing, and the agent stays off. The light agent is small on purpose. Use it as the template for your own Raspberry Pi tools. The steps (hardware module, agent, registration, packaging, tests) are in docs/AGENTS.md.

Deep research

The web agent and python3 -m threetoks.cli use the same judged loop (threetoks/research.py):

  1. Research. Run one research episode.
  2. Judge. Check the answer. Simple checks come first and need no model call. They reject an empty answer, an answer that is only digits, and an answer with no notes.
  3. Ask the model. Any other answer goes to a two-option menu: "states the facts asked for" or "vague, off-topic, or not a real answer". We use words with meaning instead of a plain yes or no. In our tests, the 1.5b model's yes/no check was almost a coin flip. A menu with meaningful words works well.
  4. Try again if needed. If the judge says no, the model proposes a new search angle. It must differ from every query already tried. Then another round runs on the same notes. So the evidence grows from round to round and does not start again.

--rounds N and /deep N set the maximum number of rounds.

An honest note. The answer is only as good as the search results. The upstream engines of SearXNG, and the DuckDuckGo fallback, slow you down or give worse results when you use them a lot. A failed run often means a bad results page, not a bad decision by the model. A local SearXNG (see infra/searxng/) helps, but it does not remove the problem.

When search fails completely, ThreeToks says so. It does not make up an answer:

  • If every search hop is blocked by a bot wall, the run ends with a "search unavailable" answer.
  • If every page failed to open, the run ends with "could not open any web page". Each failed page also shows as a fetch row in the progress tree, with the reason. The command-line tool exits with a non-zero code. ThreeToks does not write an answer from an empty note store.

Under the hood

  • Architecture. docs/DESIGN.md explains the decision nodes, the append-only prompt layout, the model and backend plan, the ladder that recovers from wrong picks, and the layout of the repository.
  • New agents. docs/AGENTS.md.
  • Default model. qwen3.5:4b through Ollama, for every node. deepseek-r1:1.5b also works (ThreeToks switches off its thinking in raw mode), but it is not the default.
  • Experiments. The folders in spikes/ hold tests of the code agent: E7 code routing, E8 the edit vertical, E9 code retrieval. The Phase 0 results (E1 to E4) are in docs/DESIGN.md §11.

What the experiments showed

Phase 0 chose the default model and the shape of the decision nodes from measured behavior, not from guesses:

  • qwen3.5:4b picks the right menu option 76.7% of the time. deepseek-r1:1.5b gets only 23 – 26%. So it cannot be the policy model.
  • The "escape" option must be the last numbered choice, not the digit 0. Tiny models almost never write a 0.
  • A retry ladder (a new temperature and a shuffled menu) fixes about half of the first-try failures.

Details and open questions: docs/DESIGN.md §11. Each folder in spikes/ has its own REPORT.md.

Tests

python3 -m unittest discover -s tests

Contributing

Read CONTRIBUTING.md first. It covers setup, how we write tests, and the design rules that you must know before you change the core. Send security problems to SECURITY.md, not to a public issue. Everyone who takes part follows the Code of Conduct.

Words used in this file
Word Meaning
Agent A helper that does one kind of job, such as web research or code.
Model The AI that makes the choices. Here it is a small language model.
Local Runs on your own computer. Your data does not leave it.
Token A small piece of text, about three quarters of a word.
Prompt The text that you send to the model.
Menu A numbered list of choices. The model answers with one number.
Router The first menu. It sends your request to the right agent.
Judge A check on an answer. It decides if the answer is good enough.
Ollama A free program that runs models on your computer.
SearXNG A private search engine that you can run yourself.
Add-on An optional part. You install it to get one feature.

Small choices. Deep research. · Released under the MIT License

About

Extremely fast & token-frugal agentic framework for small or hardware constrained LLMs. Run agents in a cave with a box of scrap. 3 Tokens per action or bust!.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages