Skip to content

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”.

A skill is a folder with a SKILL.md:

skills/
└── my-skill/
├── SKILL.md
└── scripts/ # optional
└── helper.sh
---
name: my-skill
description: A short sentence the model reads.
triggers: [keyword, other keyword]
---
# Instructions
What 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_skill tool — important for small models.
  • triggers load the skill automatically when a keyword appears in the input (skills.auto_load_on_trigger).
  • Scripts are only executable if skills.allow_scripts: true is set, and only if they were found in the scripts/ folder during discovery.

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).

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:

PackageResponsibility
pkg/poolEndpoint pool: health checks, model-aware selection, failover
pkg/routerTask category → model (heuristics + LLM classification + config)
pkg/agentTool-calling loop, salvage parser, history compaction, sessions
pkg/toolsTool interface + registry with category tags
pkg/skillsSKILL.md discovery and matching
pkg/mcpclientConnect MCP servers, adapt tools
pkg/schedCron jobs that trigger agent runs
pkg/apiHTTP 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.