Extending the platform
ollama-agent is meant as a base you take over and extend for a specific workflow. There are three levels of extension — from “no code” to “your own binary”.
Level 1: Skills (no code, no rebuild)
Section titled “Level 1: Skills (no code, no rebuild)”A skill is a folder with a SKILL.md:
skills/└── my-skill/ ├── SKILL.md └── scripts/ # optional └── helper.sh---name: my-skilldescription: A short sentence the model reads.triggers: [keyword, other keyword]---
# InstructionsWhat the model should do once the skill is loaded ...- The skill list (name + description only) sits compactly in the system prompt; the body is only pulled into context via the
load_skilltool — important for small models. triggersload the skill automatically when a keyword appears in the input (skills.auto_load_on_trigger).- Scripts are only executable if
skills.allow_scripts: trueis set, and only if they were found in thescripts/folder during discovery.
Level 2: MCP servers (configuration)
Section titled “Level 2: MCP servers (configuration)”Add any MCP server (stdio or HTTP) to the config — its tools are immediately available to the agent, named mcp_<server>_<tool>:
"mcp_servers": [ { "name": "files", "transport": "stdio", "command": "mcp-filesystem-server", "args": ["/path"], "tool_tags": ["tool_calling", "code"] }]tool_tags controls for which task categories the tools are offered to the model (tool subsetting keeps prompts small).
Level 3: Your own binary (Go library)
Section titled “Level 3: Your own binary (Go library)”The entire core lives under pkg/ and is importable. Minimal example for a custom workflow with a custom native tool:
package main
import ( "context" "encoding/json" "log/slog"
"gitlab.techeve.de/techeve/ollama-agent/pkg/agent" "gitlab.techeve.de/techeve/ollama-agent/pkg/config" "gitlab.techeve.de/techeve/ollama-agent/pkg/pool" "gitlab.techeve.de/techeve/ollama-agent/pkg/router" "gitlab.techeve.de/techeve/ollama-agent/pkg/tools")
func main() { cfg, _ := config.Load("config.json") ctx := context.Background()
p := pool.New(cfg, slog.Default()) p.Start(ctx) defer p.Close()
reg := tools.NewRegistry() reg.Register(&tools.Func{ ToolName: "ticket_lookup", Desc: "Look up a support ticket by id.", ArgsSchema: json.RawMessage(`{ "type":"object", "properties":{"id":{"type":"string"}}, "required":["id"] }`), ToolTags: []string{"tool_calling"}, Fn: func(ctx context.Context, args json.RawMessage) (string, error) { // custom logic ... return "Ticket 42: open, priority high", nil }, })
a := agent.New(agent.Options{ Pool: p, Tools: reg, Router: router.New(p, cfg.Models, nil), Config: cfg.Agent, })
res, _ := a.Run(ctx, "What is the status of ticket 42?", agent.RunOptions{}) println(res.Output)}Building blocks and their responsibilities:
| Package | Responsibility |
|---|---|
pkg/pool | Endpoint pool: health checks, model-aware selection, failover |
pkg/router | Task category → model (heuristics + LLM classification + config) |
pkg/agent | Tool-calling loop, salvage parser, history compaction, sessions |
pkg/tools | Tool interface + registry with category tags |
pkg/skills | SKILL.md discovery and matching |
pkg/mcpclient | Connect MCP servers, adapt tools |
pkg/sched | Cron jobs that trigger agent runs |
pkg/api | HTTP daemon on top of everything |
Custom tools implement the tools.Tool interface (or use tools.Func). Important for small models:
- Description: exactly one short sentence.
- Schema: keep it flat, few required fields.
- Tags: specify categories so the tool only appears in the prompt for matching tasks.
- Result: return it compactly; the loop truncates at
max_tool_result_chars.