Tools and skills

An agent does work through procedures. Orchard has three kinds, and the difference between them is not stylistic: it is what the procedure is allowed to do, who can call it, and whether the model ever sees it. A fn is a pure helper. A tool is a deterministic effect the model can call. A skill is a procedure that itself calls the model. Pick the right one and the checker keeps you honest about the rest.

Three kinds of procedure

The three differ along one axis: how much they are allowed to touch.

  • fn name(p: T) -> R is a pure helper. It may not use gen, delegate, tools, or state. It is plain computation over its arguments, and the checker proves it. A fn is not advertised to the model; it is something your own code calls.
  • tool name(p: T) -> R is a deterministic effect: an HTTP request, a shell command, or a computation. Its body may use http and shell, but never gen, delegate, or state. It is model-callable, which means the autonomous loop can invoke it by name.
  • skill name(p: T) -> R is a model-using procedure. It may call gen, run other tools, and orchestrate them. It is exposed to the loop as skill_<name>, so the model can call skill_brief for a skill named brief.

The rule of thumb: if it calls the model, it is a skill; if it has a side effect but no model call, it is a tool; if it is neither, it is a fn. The checker rejects a fn that reaches for gen, and a tool that does the same, so the boundaries are enforced rather than suggested.

Built-in packs with use

You do not write every tool yourself. use <pack> at the top of an agent pulls in a set of ready-made tools the model can call. The calculator pack is the simplest: it adds a single calculate tool that does exact arithmetic, which is worth having because models are unreliable at math and this is not.

use calculator                 // a built-in pack: gives you calculate

The built-in packs are:

  • http for outbound HTTP requests, subject to the policy's domain allowlist and the egress guard.
  • web for keyless web search and readable page fetching (it adds web_search and fetch_page).
  • shell for running shell commands through run_command, gated by the policy's allow_shell setting (off by default).
  • browser for driving a real headless Chrome over the DevTools Protocol: browser_open, browser_read (the rendered page, after JavaScript runs), browser_click, browser_type, browser_eval, and browser_screenshot. Grant it with one line, use browser; it launches Chrome with a throwaway profile and shuts it down when the run ends.
  • mcp for connecting to a Model Context Protocol server and exposing its tools to the loop.

A pack tool is dispatched exactly like one you declared yourself: same validation of arguments, same redaction of secrets in the output, same accounting against the budget.

Declaring a tool and a skill

Here is a tool and a skill side by side. word_count is a pure computation, so it is a tool with no effect at all. brief calls a pack tool and then calls the model, so it is a skill.

use calculator                 // a built-in pack: calculate

tool word_count(text: str) -> int {
    return text.split(" ").length
}

skill brief() -> str {
    let w = calculate(expression: "6 * 7")
    return gen "The answer is {w.result}."
}

Both are callable from your own code: word_count(text: msg) and brief(). Both are also advertised to the model during a delegate loop, the tool as word_count and the skill as skill_brief. A skill that mixes one constrained model call with deterministic routing around it is the central move of the language; you will see more of it in the typed generation lesson.

Annotations

Annotations tune how a procedure is presented and run. They go on the line above the declaration.

@description("Count the words in a piece of text.")
@timeout(30s)
tool word_count(text: str) -> int {
    return text.split(" ").length
}
  • @description("...") sets the human-readable description the model sees when deciding whether to call the tool. A clear description is the difference between the model using a tool correctly and ignoring it.
  • @expose(false) keeps a procedure callable from your code but hides it from the model, so it never appears in the loop's tool list.
  • @timeout(30s) bounds how long the procedure may run before it is cancelled. The argument is a duration literal.
  • @unsafe marks a procedure whose effects are not safe to retry or run without care, so the runtime treats it accordingly.

How tools reach the model

When a delegate loop starts, the runtime builds the tool list the model will see. Each model-callable tool and exposed skill becomes one entry. The runtime lowers the procedure's parameter types into a JSON schema: a str parameter becomes a string field, an int becomes an integer, a record type becomes an object with typed properties. A money parameter lowers to a "$d.dd" string and a duration to a string like "30m". The description from @description rides along.

When the model decides to call a tool, it emits JSON for the arguments. The runtime validates that JSON against the schema before the body runs, so a malformed call becomes a structured tool error the model can read and correct, not a crash. The same path handles pack tools, your declared tools, and exposed skills identically, which is why a skill the model calls behaves no differently from a built-in.

Note. A tool body may use shell and http but never gen, delegate, or state. If you find yourself wanting a model call inside a tool, what you actually want is a skill. The checker will tell you so.

Next. A procedure that only computes is forgettable; most agents need to remember things across turns and runs. Continue to Memory and state.