kowalski · architecture · 2.x
A local-first agent runtime in one binary. Work is done by hordes: small teams of agents defined as plain folders, run as durable pipelines, with tools reached over MCP and numbers computed by engines rather than models.
You download kowalski, run it, and a browser opens on the app. Everything it
needs travels inside: the operator UI, the built-in hordes, the run database. The only things
outside are the ones you choose: a model (a local Ollama, or an endpoint with your key) and the
tools you connect.
That shape is deliberate. The people kowalski is for (a small-business owner with a folder of spreadsheets, a trader with a watchlist) will not run a cluster, and should not have to trust a service with their files. So the default is dependency-light: no required daemons besides the model, SQLite on disk, and optional extras (Postgres with pgvector, graph queries) that stay off until asked for.
A horde is a directory of Markdown files. horde.md names the pipeline and
its triggers; each file in agents/ is one step (a "penguin") with TOML front matter
saying what kind of step it is, what it reads and where it writes; prompts/ holds the
instructions for the steps that use a model.
Nothing is hidden in a database or a UI-only format. A horde can be read in any editor, diffed
in git, copied to another machine, exported as a portable .kwf.zip bundle, or built
conversationally in the Rookery tab, which writes the same folder. Drop a folder next to your
config and it appears in the Hordes screen; edit a file and the running server reloads it.
| Step kind | Uses a model? | What it does |
|---|---|---|
| ingest | no | Collects the run's input: fetches the URLs (public addresses only), reads the files, keeps the text. Pages are reduced to their main content with absolute links. |
| table_profile | no | Asks the data tool for every table, its columns, types and statistics, so the next step starts from exact names. |
| sql_batch | no | Runs each SQL block of the previous step's plan through query_sql and records the results exactly as returned. |
| xlsx_report | no | Turns those results into a workbook: an index sheet and one sheet per question, numbers typed as numbers. |
| verify · apply | no | Run a check command on a project, or write proposed changes into it. Both wait for the operator's approval by default. |
| process · deliver · compile · ask · lint | yes | Model steps: a prompt plus attached context (the previous artifact, other steps' outputs), optionally with tools. The output is one Markdown file. |
Most hordes are a straight line: pipeline = ["ingest", "profile", "plan", "run", "report", "deliver"].
When a job needs branches, edges turn the pipeline into a graph: fork and join,
route on a step's pass/fail outcome, and loop back (a failed check returns to the step that
writes the fix) with a cap on how often.
The Spreadsheet analyst is the clearest example of how the parts fit, because it uses both kinds of step and the division of labour between them is the point.
Small local models are unreliable at multi-step tool use: a 7B model will often print a tool call as text instead of making it, or invent a column name. Every place where that failure would corrupt the answer is a plain step instead. The model is left with the one thing it is good at, writing a query or a paragraph from material it has been given, and a wrong query fails loudly in the engine rather than quietly in a report.
Every run is a row in a SQLite store and every transition is written before it takes effect. A restart does not lose work: interrupted runs come back with their finished steps kept, and runs started by a trigger resume on their own.
awaiting_input and says exactly what would run.
Approve continues it; the approval covers that step for the rest of the run, so a fix-and-check loop
asks once. A parked run survives restarts and never blocks a trigger.A horde can declare triggers in horde.md: a cron schedule, a watch on a
file or folder, or a webhook route. Each firing starts an ordinary durable run carrying its
input (the watched path, pre-filled form answers, the payload) and a provenance marker. Overlap is a
policy per trigger: skip, queue one, or run in parallel. Built-in schedules ship switched off, so
nothing runs, or costs, until you turn it on in the Hordes screen.
Agents reach the outside world through tools, and kowalski keeps its own set small on purpose. Capability lives in standalone MCP servers that any MCP client can use, kowalski included.
web_fetch reads one public page (private and cloud-metadata addresses refused,
every redirect re-checked). web_search uses Brave, Staan or SearXNG when configured.
File tools stay inside the folder you chose in Setup.
Every spreadsheet as a SQL table (Excel, CSV, Parquet), on Apache DataFusion. Local binary or the hosted service; Setup signs in with OAuth, so there is no token to copy.
HTTP or stdio, with bearer tokens that refresh themselves. The Docker MCP gateway brings its whole catalog through one entry. First-party servers are built on the emperor-mcp framework.
The horde builder as an MCP server: any MCP-capable assistant can design a horde, validate it and write the folder, without the kowalski UI.
Tool calls go over the model's native tool-calling interface when it has one, with a text fallback for models that do not. Each step lists the tools it may use; nothing else is offered to the model in that step.
| Tier | Holds | Default | Optional |
|---|---|---|---|
| working | The active conversation and task context | in process, bounded | — |
| episodic | A chronological log of recent interactions | SQLite file | Postgres |
| semantic | Distilled facts, found by similarity, with relations | in-process vectors | Postgres + pgvector, graph queries |
Hordes mostly do not need memory: their state is the files they write, which is easier to inspect and to trust. Memory serves the chat agent and long-lived assistants.
| Risk | Default |
|---|---|
| Someone else on the network drives your agents | On 127.0.0.1 no token is needed. Bound to any other address, the API always requires the bearer token printed at first start. |
| A step runs a command you did not expect | verify and apply wait for your approval, showing the command. [horde] confirm_commands = false turns this off for hordes you trust. |
| A workflow from elsewhere misbehaves | Imported bundles are checked like untrusted zips, their triggers arrive switched off, and every step runs in a separate process. |
| A page makes the fetcher reach inside your network | Fetches go to public addresses only, and every redirect is checked again. |
| Keys leak into a repository | The sample config is config.example.toml; your config.toml is never committed, and Setup writes it owner-only when it holds a key. |
The server binary: HTTP API, horde runner, triggers, Setup, embedded UI and built-in hordes.
Agents, model providers, tools, MCP client, memory, step handlers, manifests and bundles, the Rookery builder.
Terminal chat, bundle export and import, MCP checks, migrations, and the child process for isolated steps.
The horde builder over MCP (stdio or stateless HTTP).
Setup, Horde runs with live progress and approvals, Chat, Rookery, federation views. Built once, compiled into the server.
Pre-built binaries for macOS and Linux are attached to every release; install.sh
downloads the right one and verifies its checksum.