Architecture & Contributing
For extending the platform with your own workflows (skills, MCP, custom tools/binaries) see Extending the platform. This document describes work on the core itself.
Architecture
Section titled “Architecture” ┌────────────────────────────────────────┐ CLI (internal/cli) │ runtime (wiring) │ HTTP API (pkg/api) ──▶│ │ Scheduler (pkg/sched)─▶ pkg/agent ── Loop, Salvage, History │ │ │ │ │ ├── pkg/router Category→Model │ │ ├── pkg/tools Registry (+Tags) │ │ │ ├── builtin (load_skill …) │ │ │ └── pkg/mcpclient (Adapter) │ │ ├── pkg/skills SKILL.md │ │ └── pkg/pool Endpoint pool │ │ └── pkg/ollama HTTP client │ └────────────────────────────────────────┘ pkg/config (JSON schema for everything)Data flow of a request:
- Entry (CLI
run/chat, API, scheduler job) →agent.Run(ctx, input, opts). - Routing: explicit model? Otherwise
router.Resolve— heuristic → LLM classify (router model, JSON schema enum, temp 0) → category; then a preference list is checked againstpool.HasModel. - Prompt assembly (
agent/prompt.go): compact system prompt + short skill list + any pre-loaded skill. - Tool loop (
agent/loop.go): chat call via the pool → execute tool calls (structured or recovered via salvage) → append results asrole:tool→ repeat until a response without a tool call or the iteration cap is reached. - Pool (
pool/pool.go): selects a healthy endpoint that has the model (least-inflight, priority/latency tiebreak); on transport error/5xx, fails over to the next candidate; 4xx is passed through.
Design Principles (small models!)
Section titled “Design Principles (small models!)”Every change should respect these principles:
- Context is expensive. Keep system prompts short, tool descriptions exactly one sentence, tool subsetting via category tags (
registry.Select), load skills only on demand. - Small models make formatting mistakes. The salvage parser (
agent/parse.go) recovers tool calls from plain text: JSON in fences, JSON in prose, key variants (name/tool/action…), XML syntax (<function=x><parameter=y>). New broken formats found in practice → add them there, always with a table-driven test. - Never fail hard. Classification fails →
default_category. No model available for the category →models.default. History too large → summarize, discarding old rounds if necessary. Iteration cap reached → final call without tools. - Many small calls instead of one big one. Classification and summarization are separate mini-calls with schema-enforced output.
Directories
Section titled “Directories”| Path | Content |
|---|---|
pkg/… | public, importable platform (aim for API stability) |
internal/cli | CLI commands + runtime (wiring of all parts) |
internal/testutil/fakeollama | scriptable fake Ollama server for tests |
internal/smoke | build-tagged tests against real Ollama |
cmd/ollama-agent | main (thin) |
configs/ | example config (source: pkg/config/example.go — keep both in sync!) |
skills/ | bundled example skills |
New subcommands: a file in internal/cli/ with func init() { register(&command{...}) } — nothing more is needed.
Testing
Section titled “Testing”make test # unit tests, no networkgo test -race ./... # recommended before every pushmake vet- fakeollama covers almost everything: fake models, script responses (
Script(...)), simulate outages (SetDown), return tool calls, arbitrary behavior viaOnChat. Always test new pool/loop features against it. - Smoke tests against a real server (model must already be pulled):
OLLAMA_HOST=http://192.168.6.80:11436 SMOKE_MODEL=qwen3.5:9b make smoke- The most sensitive code is
pkg/agent/parse.go(salvage). TheTestSalvagetest is a table of real model outputs — add a line there for every new case found in practice.
Git Workflow & Release
Section titled “Git Workflow & Release”- Work happens on
develop(or feature branches → MR intodevelop). mainis protected; a release = MRdevelop→mainwith a green pipeline, followed by avX.Y.Ztag onmain.- Commits follow Conventional Commits (
feat(pool): …,fix(agent): …). - CI (
.gitlab-ci.yml): vet → test → build.
Known Limitations / Ideas for Later
Section titled “Known Limitations / Ideas for Later”- Streaming in tool rounds: the loop only streams the final response; intermediate iterations are non-streaming (a deliberate simplification).
- Sessions are in-memory (daemon restart = memory loss). Persistence (e.g. SQLite) would be a cleanly isolated feature in
pkg/agent/session.go. - Job history is in-memory; could be persisted to
state_dir. - Metrics:
/v1/healthis minimal; a Prometheus endpoint would be easy to add inpkg/api. - Embeddings exist in the client (
ollama.Embed) but are not yet wired up to the agent (RAG workflows would be a good extension candidate). - MCP resources/prompts are not used, only tools.