Tool API
Inputs, results, completion, errors and background work.
A Tool declares its name, description, input schema, optional output schema, and run function.
Call its factory to configure it: lookup() runs in the registering application by default;
lookup({ env, ...options }) selects an explicit Environment. Packaged factories carry their
executable reference. Publish a Tool
with the ordinary build and the brain-tools helper.
Application functions
This example assumes your app provides database.get(key), returning { value: string } or null.
import { tool } from "@aexhq/brain";
import { z } from "zod";
export const lookup = tool({
name: "lookup",
description: "Look up one record.",
input: z.object({ id: z.string() }),
output: z.object({ value: z.string() }).nullable(),
options: z.object({ prefix: z.string() }),
run: async ({ id }, ctx) => {
await ctx.emit("lookup_started", { id });
return ctx.finish(await database.get(`${ctx.options.prefix}${id}`) ?? null);
},
});
const placed = lookup({ prefix: "customer:" });The function runs in your application's process and can use its state, packages and credentials.
It has the same filesystem permissions and working directory as that process; creating a session
does not give it a separate workspace. Use an environment when
you need separate resources or access. Factory options are parsed and frozen; with no options
schema, call lookup(). Use lookup({ env: hostEnv({ name: "app" }), ...options }) for a named host.
Tool input JSON Schema describes what Zod accepts before parsing (io: "input"): defaulted
arguments are optional, and the handler receives their parsed defaults and transforms.
An ordinary z.object accepts and strips extra properties; z.strictObject rejects them.
Output schemas describe parsed output. Unrepresentable schemas fail when declaring the Tool.
The host
The SDK keeps a connection open so Brain can call application tools. Several sessions and tools
in the same Brain client share that connection until
await brain.close(). Ending or deleting a session releases its handlers. Put session creation
inside try and close the client in finally, including when creation fails.
The SDK validates input, calls your function and validates each output. Brain saves ordered results before making them available to the loop.
Return values and outcomes
Use return ctx.finish(value) to publish a result and finish the whole execution. Pass ordinary
output for success or an Outcome directly. An error outcome preserves
error.code, error.message, error.retryable and error.details. See the executable
Tool outcomes example.
Thrown exceptions become tool_error.
Returning and finishing are different boundaries. return value publishes one result and ends
the synchronous phase; returning nothing publishes no result. The Tool remains open for later
ctx.emit, ctx.emitResult, and ctx.finish calls. Emit before returning to make an observation
available synchronously. await ctx.finish(); return; finishes without another result.
Application tools must finish explicitly. Forgetting leaves one open until its original deadline, cancellation, or connection loss; an unlimited execution can remain open indefinitely. Normal turn completion and actor suspension preserve background Tools. Brain wakes the Agentloop for later committed observations, and the loop decides whether to call the model or wait for a user.
The Application tool extension
uses the same declarations and await ctx.finish(value) in a bounded HTTP handler. It supports
configured options, output validation and ctx.emitResult, with durable completion acknowledgments.
It grants no model calls, arbitrary events, Environment control or work beyond the request lifetime.
Existing http() / httpTool() placements retain their v1 plain-JSON return convention.
The top-level status values ok, error, timeout, cancelled and unknown are reserved for
outcome envelopes. A malformed envelope becomes invalid_output. Successful-output validation
applies to ordinary output or an explicit ok.value; non-success outcomes bypass it. If business
data itself uses a reserved status, return it as the value of an explicit ok outcome. The SDK
does not inspect that value for another outcome. Other status values remain ordinary data.
The final value must be JSON after schema transforms as well; undefined, non-finite numbers,
BigInt, and class instances are rejected. Returning a non-success outcome terminates the execution.
Context
The second run argument contains:
options: the factory options, parsed once and frozen;sessionId: the owning session, for application operations such as attachment publication;sequence: the journal sequence of thetool_call_startedrecord, which with the session id names this call everywhere;deadline: the original caller-owned absolute deadline, or undefined for unlimited execution;signal: best-effort cancellation and deadline notification;emit(kind, data): append a durable extension Event;emitResult(value, { content }?): append a validated result, optionally with model-facing text;finish(value?, { content }?): optionally append a result, then commit completion;model(request): call the session's model with independent messages and return its response;environments: authorizedlist,get,create,update,setup,deleteandcalloperations; see environment control.
Always await ctx.emit. Its promise resolves with the committed journal sequence, so an emitted
progress or audit Event is ordered before the Tool result:
run: async (input, ctx) => {
const sequence = await ctx.emit("import_started", { source: input.source });
return ctx.finish({ eventSequence: sequence, imported: await importRows(input) });
}Brain sends each invocation once. Its deadline returns timeout; explicit SDK cancellation returns
cancelled. Both are failed Tool results and describe why the caller stopped waiting, without
promising rollback. The first terminal cause is retained even if the function later finishes.
Connection loss after dispatch, without a reliable result, returns unknown. Known validation,
execution and protocol failures return error.
The SDK aborts local in-flight work and reconnects the same registered host for future commands;
it never replays the disconnected command. Cooperative functions should observe ctx.signal and
stop promptly.
Events are ordered per Tool. Finish waits for preceding admitted emissions, and later emissions are rejected. Results and finish always enter journal/history; only the Agentloop changes model messages. Brain batches wake notifications for five milliseconds from the first pending commit, without delaying commits or their acknowledgements. Further events do not extend that window.
Result presentation and independent model calls
content is optional text representing a result, including a structured error. It never replaces
the journal's structured output, bypasses output validation, or changes failure status. Agentloops
select and format it for their conversation; only the Agentloop edits or compacts that conversation.
The document summarizer demonstrates both APIs.
Tool model requests use ModelRequest and return ModelResult. Messages are explicit. Omitted
system and tools mean an empty prompt and no tools; response format has no conversation default.
The session's fixed provider, model, and credentials apply. Tool requests share the originating
activation's model-call budget, including after that activation returns. Concurrent calls retain
the Tool's identity in origin and do not emit conversational assistant deltas. Usage and outcomes
are journaled before the response returns. Completion, interruption, and the original Tool deadline
stop pending requests; an interrupted provider call has an unknown outcome and is never replayed.
HTTP Environments call the granted model execution service. Native Tools use brain:tool/host.model.
Reattaching after restart
Follow the application reattachment recipe.
brain.sessions.get(id) alone opens a handle; supplying the original tools restores their handlers
under saved host credentials. Reattachment requires exactly the original host tools and identical
advertised input/output schemas. Keep compatible handlers for existing sessions, or create a new
session when changing a contract.
Native input and output validation awaits Zod parseAsync, including defaults and transformations.
Async checks consume the invocation deadline; expiry or cancellation prevents late results. Already
expired calls stop before validation or customer code. Schema export still happens during authoring.
Brain persists the host identity and token hash; a registration with
sessions placed in it does not expire while disconnected. Restoring a host accepts future commands,
never replays old ones, and does not restore the application state captured by its functions.
A Component
import { brainEnv, component, tool } from "@aexhq/brain";
import { z } from "zod";
export const inspect = tool({
name: "inspect",
description: "Inspect one path.",
input: z.object({ path: z.string() }),
options: z.object({ depth: z.number().int().positive() }),
implementation: component(new URL("./inspect.wasm", import.meta.url)),
});
const placed = inspect({ env: brainEnv({ name: "reader", filesystem: { workspace: "read" } }), depth: 2 });Use the Rust or Python authoring examples
to build inspect.wasm. Each invocation receives input, parsed options and a deadline. Brain
uploads the file when creating a session; successful admission is cached on the Component object.
A descriptor the Environment interprets
An Environment may define its own immutable implementation descriptor:
const fetchIssue = tool({
name: "fetch_issue",
description: "Fetch one issue.",
input: z.object({ number: z.number().int() }),
options: z.object({ repository: z.string() }),
implementation: ({ repository }) => ({
type: "http_json",
method: "GET",
path: `/repos/${repository}/issues/{number}`,
}),
});
const placed = fetchIssue({ env: github, repository: "aexhq/brain" });Brain treats the descriptor as opaque and hands it to the Environment with every invoke.
Dependencies and resource access
Package dependencies with the implementation or let the chosen Environment prepare them using
its loader, lockfile, or optional setup script. Preparation must finish before import-time
dependencies or the entrypoint are used. Brain has no universal needs declaration and installs
nothing in its own process.
The application configures access through the Environment. For example,
brainEnv({ name: "reader", filesystem: { workspace: "read" } }) requests a workspace within
the server's allow-list. Use separate Environment bindings for different grants; every Tool in
one native binding receives that binding's authority. A denied grant or unsupported runtime
fails explicitly. Setup failures reach the ordinary Tool error path, without automatic retry.
Execution context
ctx.emit in a run Tool and the Component Tool's emit import both wait for the same thing: a
committed extension Event in the session journal. Brain commits a Tool intent before dispatch,
sends it once, and commits each result, return, and finish before the Agentloop observes it.
Result and extension emissions share the originating activation's BRAIN_MAX_EMITTED_BYTES
budget. Returning or waking a later activation does not renew a Tool's budget or deadline.
There is no automatic retry.
Native binding implementers
The optional binding specification is @aexhq/brain/contracts/tool.wit (brain:tool@0.2.0).
emit-result takes an Outcome JSON string; finish takes an optional Outcome JSON string.
returned ends the synchronous phase while exported run may keep executing. Call finish
before returning for an ordinary synchronous tool. deadline-at-ms = 0 means unlimited.
Native invocation fields do not expose the application's ctx.sessionId or ctx.sequence.
Telemetry is best-effort diagnostics and does not enter saved history or model context.