Write a tool
Add a function the agent can call, then run it in your app or package it for Brain.
Start with a JavaScript or TypeScript function. The example below returns sample order data; replace its body with your database or API call.
TypeScript and JavaScript
Complete the quickstart first. Save this as order-tool.ts beside
order.ts. The same code works as JavaScript:
import { tool } from "@aexhq/brain";
import { z } from "zod";
export const lookupOrder = tool({
name: "lookup_order",
description: "Look up an order by id.",
input: z.object({ id: z.string() }),
run: ({ id }, ctx) => ctx.finish({ id, status: "shipped" }),
});Replace the quickstart's inline declaration with import { lookupOrder } from "./order-tool.js".
Run npx tsx order.ts; NodeNext compilation resolves the .js import to the TypeScript source.
Keep the session's tool placement:
tools: [lookupOrder()],The transcript includes the lookup result and an answer about the order. The tool runs in your process and can use its packages, credentials and working directory. It has no separate session workspace. Keep that process connected until the tool finishes; choose HTTP or remote execution when it should run independently.
Publish a tool library
Write and export tool({ ..., run }) using your normal TypeScript or JavaScript build. The
runnable package example
contains a filesystem tool, a normal tsc build, and two package exports. Clone the repository and
run these commands from examples/packaged-tools, not the repository root. brain.tools in its
package.json selects the executable export and factory names. After compilation, brain-tools
generates the public client entry and its types. Build and inspect the release with:
npm install
npm run build
npm packConsumers import the factory from the package and call readText({ env: workspace }). Its browser
entry does not import the filesystem implementation. readText() loads the executable in the
registering app when that runtime supports it. The selected Environment prepares ordinary npm
dependencies and loads the exact package version; an unavailable package or incompatible placement
fails explicitly. Neither the Tool author nor its consumer writes an execution descriptor.
Match the package's Brain SDK version to its consumers. Before publishing, follow the example's
fresh consumer check
to catch incompatible factory and environment types. readText() uses the application's current
working directory and permissions; remote placement follows that provider's workspace rules.
For a service that loads packages itself, see Write an environment.
Supply useful model context
Finish with the full structured value and optional short text: ctx.finish(value, { content: summary }).
ctx.emitResult accepts the same option for partial results. The Agentloop decides where to place
that text and when to compact it.
A Tool can call ctx.model with its own messages. The
document summarizer is a complete
example. Its request returns to that Tool; the Agentloop still owns the shared conversation.
Language choices
Other languages can supply JSON Schema metadata directly through bindTool; no Zod schema
reconstruction is needed. The Python package example
exports metadata from Python and runs in a prepared Python Environment. Its JavaScript caller
is exercised in the cross-language test.
| Language | Where it runs | Build path |
|---|---|---|
| JavaScript / TypeScript | Your application (hostEnv) | The example above |
| Rust | Brain (brainEnv) | Build the Rust example below |
| Python | Brain (brainEnv) | Package the Python example below |
| A language in your own runtime | Your environment service | Write an environment |
Rust and Python produce a .wasm extension file. You only need that packaging step when you
want Brain to run the tool independently of your app. These are tested examples, not a promise
that every library in those languages runs there.
Rust
Clone Brain and start from
tests/fixtures/diagnostic-tool.
Its echo path finishes with a JSON result:
fn finish(value: serde_json::Value) -> Result<String, ToolError> {
brain::tool::host::finish(Some(
&serde_json::json!({"status": "ok", "value": value}).to_string(),
))?;
Ok(value.to_string())
}The linked project includes the input handler, bindings and Cargo.toml. Edit its run
function, then build from the Brain checkout with Rust 1.97 or newer:
rustup target add wasm32-wasip2
cargo build --manifest-path tests/fixtures/diagnostic-tool/Cargo.toml --target wasm32-wasip2 --releaseCopy tests/fixtures/diagnostic-tool/target/wasm32-wasip2/release/diagnostic_tool.wasm
into your app as echo.wasm.
Python
The complete Python tool returns its input and records a progress event:
import json
from wit_world import WitWorld
from wit_world.imports import host
class WitWorld(WitWorld):
def run(self, input):
value = json.loads(input.input_json)
sequence = host.emit("python_tool_ran", json.dumps(value))
result = {"echo": value, "sequence": sequence}
host.finish(json.dumps({"status": "ok", "value": result}))
return json.dumps(result)Edit tests/fixtures/python/tool.py in a Brain checkout. With Python 3, venv and Bash available
(Linux or WSL), build it with:
bash tools/build-python-fixtures.sh "$PWD/build/python"The script installs the pinned builder and generates the imports. Copy
build/python/python-tool.wasm into your app as echo.wasm.
This example supports Python computation and Brain callbacks, not Python filesystem or network
APIs. The server needs BRAIN_MAX_CORE_INSTANCES=1024; add that environment variable to the
quickstart's Docker command. This requires a Brain server you control. For hosted runtimes, see
the Aex docs.
Use a Python environment service
for OS packages and ordinary file/network access.
Use the built tool
Replace the quickstart's tool declaration and placement with:
import { component } from "@aexhq/brain";
const echo = tool({
name: "echo",
description: "Echo a message.",
input: z.object({ message: z.string() }),
implementation: component(new URL("./echo.wasm", import.meta.url)),
});
// In sessions.create:
// tools: [echo({ env: brainEnv({ name: "brain" }) })],Send "Call echo with the message hello." and run the app. The transcript should contain
hello in the tool result. The SDK uploads the file when it creates the session.
Return values and outcomes
Use ctx.finish(value) for ordinary successful output. Throw an error when the function cannot
complete. Tools that continue in the background use ctx.emitResult(value) and eventually
ctx.finish(). Returning alone leaves the tool open. Observe ctx.signal to stop on cancellation.
See the tool contract for structured errors, reserved status values, deadlines, background results and host reconnection.