Tools and skills
Tools¶
A Tool carries its own implementation:
type Tool struct {
Name string
Description string
Schema json.RawMessage
Invoke func(ctx context.Context, args json.RawMessage) (string, error)
}
Invoke is a closure you build, so an MCP server, a credential, a database
handle and a network call all stay on your side. The library marshals the
arguments the model produced, calls the function, and puts what it returns back
in the conversation as a tool message. It never learns what the tool talks to.
A tool that returns an error is not fatal. The error text becomes the tool result, the model sees it, and the run continues — a model that can read "repository not found" usually recovers, and a run that dies on the first bad argument cannot.
Skills¶
A skill is a name, a description, and a body that is only fetched if asked for:
agent.Skill{
Name: "commit-style",
Description: "How commits are written here. Use before writing one.",
Body: func(ctx context.Context) (string, error) {
return os.ReadFile("skills/commit-style.md")
},
}
Descriptions go into the system prompt as a listing. Bodies do not. When the
model wants one it calls the Skill tool with the name, and only then does the
body run — which means it can be a file read, a database query or an HTTP call,
paid for only when used.
This is progressive disclosure, the way Claude Code loads skills, and the combined description is truncated at 1,536 characters for the same reason it is there.
Why one tool and not one per skill¶
Sixty skills cost one entry in the tool list.
The tool array is the front of the cached prefix — a provider caches
tools → system → messages in that order, so a difference in the tools
invalidates everything after it. Declaring a tool per skill would give every
caller a different tool array, and none of them would share a cache entry. With
one Skill tool the array is byte-identical across callers, and only the
listing differs, one block later.
Info
Two cache breakpoints is the design, not a compromise: the tool list is shared by everyone, the skill listing is shared by everyone offered the same skills.
What the loop guarantees¶
- A skill loaded twice returns a note the second time rather than the body again, so a confused model cannot spend the same tokens repeatedly
- A body that failed is not marked loaded, so it can be retried
- Two skills with the same name, or one with no name, is refused when the run starts rather than confusing the model later
- A tool already called
Skillis refused for the same reason