bots-garden/mini-mepublic Fork
⑂ v0.8.0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 5988c55 · on v0.8.0 · k33g · 6d ago
declared-tools.md · 54 lines · 6.3 KBmarkdown
Blame HistoryOpen raw

Declared tools — explanation

What is this about?

mm had two ways to let the model run a command of yours before ticket 88: a skill that says "call your bash tool with this command", and an MCP server exposing a tool. Declared tools are a third: a tools: entry in the agent file that turns one command into a typed tool, with no server to write. This page explains why it was added beside skills rather than instead of them, and why it is shaped the way it is.

Why skills were not enough

A skill runs a command in two steps. The model must first find the skill in the catalogue and call read_skill, then compose a bash command line from what it read. Each step can fail, and on a small model they do:

  • The skill is never found. The catalogue is routed by the model reading names, descriptions and keywords. Measured on mellum2-12b, a status skill ("Report the status of one of the company's services") was never read for "What is the status of the billing service?" — 0 out of 10 runs, in English and in French. The model improvised curl localhost:8080/health and ps aux | grep billing, and then answered that the service "seems to be running". That is a wrong answer, not just a miss.
  • The command is rewritten. When the skill is skipped but the model finds the script anyway, it guesses the arguments: ./pkgtest.sh greet instead of ./greet.
  • Consent is all or nothing. Everything a skill runs goes through bash, so "always allow this script" can only be said as "always allow bash".

A declared tool removes the routing step: its name, description and parameters are in every request, like bash's.

What was measured

Three actions (release <version> <channel>, pkgtest <package>, status <service>), each asked in English and in French, 5 runs each, mm -p, on two models. A run succeeds when the script really ran with exactly the expected arguments — checked from a file the script writes, not from the model's answer.

Model Skill + bash Typed tool through an MCP stand-in Declared tool
mellum2-12b 15/30 (4.5 s on average) 25/30 (2.8 s) 30/30 (1.3 s)
qwen3.5-4b 30/30 (12.7 s) 30/30 (7.7 s) 30/30 (5.8 s)

The MCP stand-in was measured first, before any code was written: a small MCP HTTP server exposing the same three tools gave the same comparison without building anything. It decided the question. The declared tools were measured again once built.

Two things stand out. First, a typed tool is faster even when the skill works: one model round trip instead of two. Second, the stand-in's five failures on mellum2 — the French release request — came from the model packing every argument into one field: {version: "1.4.2 1.4.2, channel beta"}. With the declared tool, the same request succeeded 5 times out of 5, each time in one call. The declared tool differs from the stand-in in several ways at once — no proj_ name prefix, no "MCP tool from server …" in the description, a closing "Give each parameter as its own field." — and which of them matters was not measured separately.

The numbers come from one small fixture, two models and five runs per case. They answer "is it worth building?", not "how much better is it in general?".

Why it is designed this way

No shell. The command is a list, one argv entry per element, and a value replaces its {{placeholder}} inside one entry. A model's value — or a value a prompt injection put in the conversation — cannot add an argument, chain a second command or redirect an output: 1.4.2; rm -rf ~ reaches the script as one odd version string. A template like command: "./release.sh {{version}}" run by bash -c was rejected for exactly that reason. A command that needs a pipe goes into a script.

The program cannot come from a parameter. Otherwise a declared tool is bash with extra steps.

Invalid calls are answered, not rejected by the schema. The schema the model reads carries the types, but not required nor enum as keywords — those go into the descriptions. Genkit validates a call against the declared schema before running the tool, and a call it rejects ends the turn. mm prefers to answer in text, saying what was wrong, what the model sent and what each parameter should be, so the model can try again. The same choice was made for MCP tools. Quoting the call back matters: told only "channel is missing", mellum2 sent the same packed call six times until the loop detector stopped the turn.

Lenient coercion, nothing more. Models write "3" for 3 and "true" for true often enough that those strings are accepted. Anything that would need a guess ("yes", "three") is refused with the reason.

Consent follows yolo. A declared tool runs something, so with yolo: false it asks like bash; with yolo: true it does not. Asking "even in yolo" was considered and left out: the agent's author wrote the command, which is already a stronger statement than a model composing a bash line.

Names are reserved early. A declared tool cannot take a built-in name even when that tool is turned off, so turning it on later does not break the file. A clash with an MCP tool can only be seen once the server answered, so it is a start-up warning, not an error.

Rejected alternatives

  • Only skills. Cheaper to write and fine for procedures, but measured to fail where it matters on the small models mm targets.
  • Only MCP. Already gives typed tools, but each one needs a server to write, start and keep running, over HTTP only (mm has no MCP stdio transport). Too heavy for "run this script with two arguments".
  • A Wasm plugin system (ticket 46). Sandboxed tools are a different question; a declared tool runs with the user's rights, like bash.

How it relates to the rest

Declared tools sit beside the built-in tools: the same loop detector, the same output truncation and echo, the same working directory and cancellation as bash, the same permission dialogs (see yolo and permissions), and the preToolCall / postToolCall hooks. Skills remain the place for know-how and procedures; a skill can also tell the model to call a declared tool.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Declared tools — explanation

## What is this about?

mm had two ways to let the model run a command of yours before ticket 88: a [skill](../how-to/add-a-skill.md) that says "call your `bash` tool with this command", and an MCP server exposing a tool. Declared tools are a third: a `tools:` entry in the agent file that turns one command into a typed tool, with no server to write. This page explains why it was added beside skills rather than instead of them, and why it is shaped the way it is.

## Why skills were not enough

A skill runs a command in two steps. The model must first find the skill in the catalogue and call `read_skill`, then compose a `bash` command line from what it read. Each step can fail, and on a small model they do:

- **The skill is never found.** The catalogue is routed by the model reading names, descriptions and keywords. Measured on mellum2-12b, a `status` skill ("Report the status of one of the company's services") was never read for "What is the status of the billing service?" — 0 out of 10 runs, in English and in French. The model improvised `curl localhost:8080/health` and `ps aux | grep billing`, and then answered that the service "seems to be running". That is a wrong answer, not just a miss.
- **The command is rewritten.** When the skill is skipped but the model finds the script anyway, it guesses the arguments: `./pkgtest.sh greet` instead of `./greet`.
- **Consent is all or nothing.** Everything a skill runs goes through `bash`, so "always allow this script" can only be said as "always allow bash".

A declared tool removes the routing step: its name, description and parameters are in every request, like `bash`'s.

## What was measured

Three actions (`release <version> <channel>`, `pkgtest <package>`, `status <service>`), each asked in English and in French, 5 runs each, `mm -p`, on two models. A run succeeds when the script really ran with exactly the expected arguments — checked from a file the script writes, not from the model's answer.

| Model | Skill + `bash` | Typed tool through an MCP stand-in | Declared tool |
|-------|----------------|-------------------------------------|---------------|
| mellum2-12b | 15/30 (4.5 s on average) | 25/30 (2.8 s) | 30/30 (1.3 s) |
| qwen3.5-4b | 30/30 (12.7 s) | 30/30 (7.7 s) | 30/30 (5.8 s) |

The MCP stand-in was measured first, before any code was written: a small MCP HTTP server exposing the same three tools gave the same comparison without building anything. It decided the question. The declared tools were measured again once built.

Two things stand out. First, a typed tool is faster even when the skill works: one model round trip instead of two. Second, the stand-in's five failures on mellum2 — the French release request — came from the model packing every argument into one field: `{version: "1.4.2 1.4.2, channel beta"}`. With the declared tool, the same request succeeded 5 times out of 5, each time in one call. The declared tool differs from the stand-in in several ways at once — no `proj_` name prefix, no "MCP tool from server …" in the description, a closing "Give each parameter as its own field." — and which of them matters was not measured separately.

The numbers come from one small fixture, two models and five runs per case. They answer "is it worth building?", not "how much better is it in general?".

## Why it is designed this way

**No shell.** The command is a list, one argv entry per element, and a value replaces its `{{placeholder}}` inside one entry. A model's value — or a value a prompt injection put in the conversation — cannot add an argument, chain a second command or redirect an output: `1.4.2; rm -rf ~` reaches the script as one odd version string. A template like `command: "./release.sh {{version}}"` run by `bash -c` was rejected for exactly that reason. A command that needs a pipe goes into a script.

**The program cannot come from a parameter.** Otherwise a declared tool is `bash` with extra steps.

**Invalid calls are answered, not rejected by the schema.** The schema the model reads carries the types, but not `required` nor `enum` as keywords — those go into the descriptions. Genkit validates a call against the declared schema before running the tool, and a call it rejects ends the turn. mm prefers to answer in text, saying what was wrong, what the model sent and what each parameter should be, so the model can try again. The same choice was made for MCP tools. Quoting the call back matters: told only "channel is missing", mellum2 sent the same packed call six times until the [loop detector](../reference/tools.md#loop-detection) stopped the turn.

**Lenient coercion, nothing more.** Models write `"3"` for 3 and `"true"` for true often enough that those strings are accepted. Anything that would need a guess (`"yes"`, `"three"`) is refused with the reason.

**Consent follows `yolo`.** A declared tool runs something, so with `yolo: false` it asks like `bash`; with `yolo: true` it does not. Asking "even in yolo" was considered and left out: the agent's author wrote the command, which is already a stronger statement than a model composing a `bash` line.

**Names are reserved early.** A declared tool cannot take a built-in name even when that tool is turned off, so turning it on later does not break the file. A clash with an MCP tool can only be seen once the server answered, so it is a start-up warning, not an error.

## Rejected alternatives

- **Only skills.** Cheaper to write and fine for procedures, but measured to fail where it matters on the small models mm targets.
- **Only MCP.** Already gives typed tools, but each one needs a server to write, start and keep running, over HTTP only (mm has no MCP stdio transport). Too heavy for "run this script with two arguments".
- **A Wasm plugin system** (ticket 46). Sandboxed tools are a different question; a declared tool runs with the user's rights, like `bash`.

## How it relates to the rest

Declared tools sit beside the built-in tools: the same loop detector, the same output truncation and echo, the same working directory and cancellation as [`bash`](../reference/tools.md#bash), the same permission dialogs (see [yolo and permissions](yolo-and-permissions.md)), and the `preToolCall` / `postToolCall` [hooks](hooks.md). Skills remain the place for know-how and procedures; a skill can also tell the model to call a declared tool.