Skip to content
BeeuniBeeuni
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

Tools

One tool module runs on every agent harness. beeuni.yaml lists the Beeuni tools and the project's own tools, and each agent's tools list decides who may call them.

A tool is a function the model calls during a turn. A session has three kinds, on both harnesses, default runtime and experimental runtime:

Kind Names Implemented by
Harness tools bash, read, write, edit, glob, grep, task, question, … The harness.
Beeuni tools web_search, image_search, scrape_webpage, memory, show Beeuni. beeuni.yaml lists each one as <name>: beeuni:<name>. A project with no tools: key gets all five.
Project tools Any name you declare Your project: one module, declared under tools: in beeuni.yaml.

Beeuni tools and project tools are harness-neutral. The sandbox daemon loads one module per tool and runs it for either harness, with the same name, arguments, and output. The experimental runtime calls the module in the daemon’s process. The default runtime calls it through a plugin the daemon writes, which forwards each call to the daemon. A module never imports a harness package.

Beeuni tools in beeuni.yaml

A new project lists the five Beeuni tools under tools::

tools:
  web_search: beeuni:web_search      # maintained by Beeuni, updated automatically
  image_search: beeuni:image_search
  scrape_webpage: beeuni:scrape_webpage
  memory: beeuni:memory
  show: beeuni:show
  # lookup_order: tools/lookup_order.ts   # your own tool: a module in your repo

beeuni:<name> means: the tool Beeuni maintains. A fix to it reaches every project that lists it, with no change in the project.

Which Beeuni tools a session gets

beeuni.yaml The session gets
No tools: key, in the root file or in any imported file All five Beeuni tools. A project created before this list existed keeps them all.
A tools: key, even an empty one (tools: {}, or tools: with every line deleted) Only the Beeuni tools it lists, as beeuni:<name> or as a module that replaces the tool.

The project’s own tools load in both cases.

See what a session gets

beeuni tools ls reads beeuni.yaml and its imports and prints each tool a session gets, then the Beeuni tools the project removed:

$ beeuni tools ls
NAME            KIND                 SOURCE
web_search      beeuni (overridden)  tools/web_search.ts
scrape_webpage  beeuni               beeuni:scrape_webpage
memory          beeuni               beeuni:memory
show            beeuni               beeuni:show
lookup_order    project              tools/lookup_order.ts

Removed Beeuni tools: image_search
Add `<name>: beeuni:<name>` under `tools:` to get one back.
Kind Meaning
beeuni A Beeuni tool, listed as beeuni:<name> (or every one, with no tools: key).
beeuni (overridden) A Beeuni tool name with a module path: the project’s module runs instead.
project A tool of the project’s own.

beeuni tools ls --json prints { manifest, tools_key, tools: [{ name, kind, source }], removed }.

Remove a Beeuni tool

Delete its line under tools:. Every agent of the project loses the tool, on both harnesses, in every session that starts after the change request merges. To keep a tool from one agent only, use agents.<name>.tools instead.

A session of a project that removed a tool:

  • default runtime does not offer the tool to the model.
  • experimental runtime does not offer the tool. A call to it fails with Tool show not found (for show).
  • The daemon answers POST /beeuni/tools/<name> with 404 and no tool named <name> is loaded.
  • A skill that names the tool still loads. For example, beeuni-memory names memory. The model reads the skill, finds no such tool, and uses other tools: without memory it reads and edits the files under memory/ with the file tools.

Change a Beeuni tool: eject

beeuni tools eject <name> gives the project its own copy of a Beeuni tool:

$ beeuni tools eject web_search
  ✓  Ejected web_search
  wrote   tools/web_search.ts (the Beeuni source, now the project's to change)
  set     beeuni.yaml: tools.web_search: tools/web_search.ts

Next:
  1. Edit tools/web_search.ts. Keep the default export: { description, parameters, execute(args, context) }.
  2. beeuni validate
  3. git add tools/web_search.ts beeuni.yaml && git commit -m "Own the web_search tool"
  4. Open a change request: git push origin HEAD && beeuni cr open --title "Own the web_search tool"
Sessions load the copy after the change request merges.
  • It writes the Beeuni source, the same bytes the sandbox runs, to tools/<name>.ts. The file imports only node:* modules and reads BEEUNI_API_URL, BEEUNI_TOKEN, and provider keys from context.env, so it runs as it is.
  • It sets <name>: tools/<name>.ts in beeuni.yaml, in the file that declares the entry (the root file or an imported file). Every other line and comment stays. The changed line ends with # ejected from beeuni:<name>.
  • With no tools: key, it creates one and lists the other four Beeuni tools as beeuni:<name>, so the eject removes nothing.
  • It refuses when tools/<name>.ts exists, or when the tool already points at another module. --force overwrites.
  • It refuses a name that is not a Beeuni tool.

The copy is yours. Beeuni no longer updates it. To return to the maintained tool, set the line back to <name>: beeuni:<name> and delete the file.

Override without eject

A Beeuni tool name with a module path replaces the Beeuni tool, on every harness:

tools:
  web_search: integrations/search/web_search.ts

The web UI views

The web UI has a view for each Beeuni tool, picked by the tool name. The show and memory views read the call’s arguments (type, path, url, content, items; command, path). The web_search, image_search, and scrape_webpage views read the output JSON. A changed copy that keeps the name must keep those arguments and that output shape, or the UI shows the raw output.

Validation

beeuni validate checks the list:

  • <name> in beeuni:<name> must be a Beeuni tool, and the key must equal it. web_search: beeuni:image_search is an error: must be `beeuni:web_search` (the Beeuni tool) or a repo-relative path to a .ts or .js module (e.g. `tools/web_search.ts`) that replaces it.
  • A tools: key that lists no Beeuni tool is a warning: Sessions of this project get no Beeuni tool (web_search, image_search, scrape_webpage, memory, show). Add `<name>: beeuni:<name>` to keep one. It catches a project that adds its first own tool and drops the five by accident.
  • An agent whose tools names a Beeuni tool the project does not load is a warning: "show" is a Beeuni tool this project does not load: add `show: beeuni:show` under the top-level `tools`, or remove it here.

The manifest reference has every rule.

Write a project tool

// tools/lookup_order.ts
export default {
  description: 'Look up an order by id. Returns its status and carrier.',
  parameters: {
    type: 'object',
    properties: {
      id: { type: 'string', description: 'The order id, e.g. "A-1042"' },
      include_items: { type: 'boolean', description: 'Also list the items' },
    },
    required: ['id'],
  },
  async execute(args, context) {
    const response = await fetch(`https://orders.example.com/v1/orders/${args.id}`, {
      headers: { authorization: `Bearer ${context.env.ORDERS_API_KEY}` },
      signal: context.signal,
    })
    if (!response.ok) throw new Error(`orders API answered ${response.status}`)
    return await response.json()
  },
}
# beeuni.yaml
tools:
  web_search: beeuni:web_search
  # … the other Beeuni tools you keep
  lookup_order: tools/lookup_order.ts

The default export is a plain object:

Field Rule
description Non-empty string. The model reads it to decide when to call the tool.
parameters JSON Schema of the arguments, type: 'object'. The model receives it as it is, so list required arguments in required. Beeuni does not validate the arguments against it.
execute(args, context) Returns a string, or any JSON value (sent as indented JSON). May be async. A thrown error is the tool’s error, and the model reads its message.

context carries:

Field What it is
sessionId The Beeuni session the call belongs to.
agent The agent that made the call.
directory The project checkout, /workspace. Resolve a relative path against it.
env The box environment plus the secrets this agent receives, as they are now. Read a secret as context.env.NAME: a secret changed after the box booted is in context.env, not in process.env.
signal An AbortSignal that aborts when the user stops the turn. Pass it to fetch.
  • Output size. An output over 50 KB or 2,000 lines is saved whole to a file in the sandbox’s temp directory. The model receives the head, the size, and the file path. Return what the model needs, not a raw dump.
  • Errors. A thrown error ends the call, and the model reads the error message. Throw a message that says what to change.
  • Imports. A module may import node:* modules, Bun APIs, and relative files. A package resolves from a node_modules next to the module or above it. The sandbox installs no package for a tool, and a config release holds only the files Git tracks. Use fetch for HTTP APIs. TypeScript runs without a build step.
  • Failures. A module that does not load (a missing file, no default export, a parameters that is not an object schema, an error at import, a package that does not resolve) is left out, and the other tools still load. The daemon logs [tools] project tools that did not load with the reason per tool.

parameters with zod

zod 4 writes the same JSON Schema:

import { z } from 'zod'

export default {
  description: 'Look up an order by id.',
  parameters: z.toJSONSchema(z.object({ id: z.string().describe('The order id') })),
  async execute(args) {
    return `order ${args.id}: shipped`
  },
}

This module loads only where zod resolves from a node_modules in the checkout the session runs. Otherwise it fails with Cannot find package 'zod'. To write the schema with zod and ship plain JSON Schema, print it once and paste the output into parameters:

bun -e "import { z } from 'zod'; console.log(JSON.stringify(z.toJSONSchema(z.object({ id: z.string() })), null, 2))"

Example: call an HTTP API with a project secret

Store the key as a project secret. The command reads the value from stdin:

beeuni secrets set WEATHER_API_KEY=-

Grant the secret to the agents that call the tool. The starter’s beeuni agent has secrets: all. A scoped agent lists it: secrets: [WEATHER_API_KEY].

Write the module:

// tools/weather_now.ts
export default {
  description: 'Current weather for a city: temperature in °C and conditions.',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string', description: 'City name, e.g. "Berlin"' } },
    required: ['city'],
  },
  async execute(args, context) {
    const key = context.env.WEATHER_API_KEY
    if (!key) throw new Error('WEATHER_API_KEY is not set: run `beeuni secrets set WEATHER_API_KEY=-` and grant it to this agent.')
    const url = `https://api.weather.example.com/v1/current?city=${encodeURIComponent(args.city)}`
    const response = await fetch(url, { headers: { authorization: `Bearer ${key}` }, signal: context.signal })
    if (!response.ok) throw new Error(`weather API answered ${response.status}: ${await response.text()}`)
    const data = await response.json()
    return { city: args.city, temperature_c: data.temp_c, conditions: data.conditions }
  },
}

Declare it:

tools:
  # … the Beeuni tools you keep
  weather_now: tools/weather_now.ts

beeuni tools eject web_search writes tools/web_search.ts and sets web_search: tools/web_search.ts. Then change the copy. For example, to keep results from one site only, add the site to each query in execute, before the request:

// tools/web_search.ts, inside execute()
const queries = String(args.query ?? '').split('|||').map((query) => query.trim()).filter(Boolean)
  .map((query) => `site:docs.example.com ${query}`)

Keep the name, the arguments, and the output JSON, so skills that name web_search and the web UI view still work.

Test a tool

beeuni validate checks the name, the path, a missing file, and the beeuni: values. beeuni tools ls shows what a session gets.

Then commit, push, and open a change request. After it merges, start a session and ask for one call with real arguments:

beeuni sessions new --wait --prompt "Call the weather_now tool once with city Berlin and print its output."

A tool that does not load is not offered to the model. A tool that throws answers with its error message. beeuni sessions log <id> prints the session’s messages, with each tool call and its status.

Declare tools

tools: maps each tool name to its module, or a Beeuni tool name to beeuni:<name>. The folder of a module is yours to choose.

tools:
  web_search: beeuni:web_search
  memory: beeuni:memory
  lookup_order: tools/lookup_order.ts
  crm_note: integrations/crm/note.ts
  • A name is snake_case, at most 64 characters: ^[a-z][a-z0-9_]{0,63}$.
  • A harness tool name is an error. A Beeuni tool name takes beeuni:<name> or a module path; the module replaces the Beeuni tool, on every harness.
  • A path is repo-relative and ends in .ts, .js, .mts, .mjs, .cts, or .cjs.
  • A file listed under imports: may declare tools. A name declared in two files is an error. A tools: key in an imported file counts as a tools: key for the selection rule.

The manifest reference has the full rules.

Who may use which tool

agents.<name>.tools decides which tools an agent’s model sees. It names harness, Beeuni, and project tools alike.

agents:
  beeuni:
    tools: all                                    # every tool (the default)
  researcher:
    tools: [read, grep, web_search, lookup_order] # only these
  support:
    tools:
      exclude: [bash, write, edit]                # every tool but these
  greeter:
    tools: none                                    # no tool
Value The agent gets
omitted, or all Every tool.
none No tool.
a list Only the tools listed. A list with * is all.
{ exclude: [...] } Every tool except the ones listed.
{ bash: false, … } The earlier map form: false removes a tool.
  • tools decides visibility. The agent’s permission rules still decide whether a call is allowed, asked, or denied. A tool not in the list is hidden from the model and denied if called.
  • An agent list cannot add a Beeuni tool the project does not load. List it under the top-level tools: first.
  • A subagent started with the task tool: on the experimental runtime it gets only the tools both its own list and the session agent’s list allow. on the default runtime it gets the tools of its own list: default runtime applies an agent’s rules to that agent only. To stop an agent from delegating, leave task out of its list.
  • Connector tools follow agents.<name>.connectors, not this list.
  • beeuni validate warns about a name that is not a harness, Beeuni, or project tool, and about a Beeuni tool the project does not load. A typo in an exclude list leaves the real tool on.

on the default runtime, bash also covers the terminal plugin’s pty_* tools, and write, edit, and apply_patch share one permission: listing one shows all three, and excluding one hides all three. The experimental runtime has separate write and edit tools.

When a change takes effect

A session loads its tools when its runtime starts. A change applies after the change request merges: new sessions load it, and with config releases a running session loads it at the next convergence. on the experimental runtime, a change to a project tool or to the list of Beeuni tools restarts the runtime in place while no turn runs; the conversation continues.

Tools in harnesses/opencode/tools/

The default runtime still loads custom tools from its config directory. The experimental runtime does not. Projects created before hosted tools carry copies of the Beeuni tools there (web_search.ts, memory.ts, …).

  • on the default runtime, a tool in that folder wins over the hosted tool of the same name, so a project keeps its copy until it deletes it. Delete the copies of the Beeuni tools: the daemon hosts the ones beeuni.yaml lists, on both harnesses.
  • To make an default runtime tool run on both harnesses, move it out of the folder, replace tool({ description, args, execute }) with a plain object whose parameters is JSON Schema, read secrets from context.env, and declare it under tools:.

Was this page helpful?