Write a tool
Add a function the agent can call, then run it in your app or package it for Brain.
Start with a plain JavaScript or TypeScript function. The example below returns sample order data; swap its body for your database or API call.
TypeScript and JavaScript
Finish the quickstart first, then save this as order-tool.ts next to
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 shows 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, or choose HTTP or remote execution when it should run independently.
Publish a tool library
Write and export tool({ ..., run }) with your normal TypeScript or JavaScript build. The
runnable package example has a
filesystem tool, a standard 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, and after compilation
brain-tools generates the public client entry and its types. Build and inspect the release:
npm install
npm run build
npm packConsumers import the factory from your package and call readText({ env: workspace }). The browser
entry never imports the filesystem implementation. readText() loads the executable in the
registering app when that runtime supports it. The chosen Environment installs ordinary npm
dependencies and loads the exact package version; a missing package or incompatible placement fails
with a clear error. Neither you nor your consumers write an execution descriptor.
Keep the package's Brain SDK version in step with its consumers. Before publishing, run the
example's fresh consumer check
to catch incompatible factory and environment types. readText() uses the app's current working
directory and permissions; remote placements follow that provider's workspace rules.
For a service that loads packages itself, see Write an environment.
Give the model useful context
Finish with the full structured value plus optional short text: ctx.finish(value, { content: summary }).
ctx.emitResult takes the same option for partial results. The Agentloop decides where that text
goes and when to compact it.
A Tool can also call ctx.model with its own messages; see the
document summarizer for a
complete example. The response goes back to that Tool, and the Agentloop still owns the shared
conversation.
Language choices
Other languages can supply JSON Schema metadata directly through bindTool, with no need to rebuild
a Zod schema. The Python package example
exports metadata from Python and runs in a prepared Python Environment. Its JavaScript caller is
covered by 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 Brain
should 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 (Linux or
WSL), build it:
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 covers Python computation and Brain callbacks, not Python filesystem or network APIs.
The server needs BRAIN_MAX_CORE_INSTANCES=1024, so add that 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
when you need OS packages or ordinary file and 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 tool result in the transcript
should contain hello. The SDK uploads the file when it creates the session.
Return values and outcomes
- Success: call
ctx.finish(value)with the output. - Failure: throw an error when the function can't complete.
- Background work: call
ctx.emitResult(value)as results arrive, thenctx.finish(). Returning alone leaves the tool open. - Cancellation: watch
ctx.signaland stop when it fires.
See the tool contract for structured errors, reserved status values, deadlines, background results and host reconnection.