Write an agent loop
Customize model calls, tool use and conversation history, then run your loop in a session.
An agent loop chooses what the model sees and what happens next. Use a ready-made loop if you only need a prompt or different tools; choose Pi or Codex first. Write your own when you need different context management, tool sequencing or stopping rules.
A minimal JavaScript loop
Start in the quickstart app with Node.js 22 or newer. Save this as
loop.mjs. It calls the model once per message and saves the conversation; it does not call tools.
import { defineAgentloop } from "@aexhq/brain/agentloop";
export const turn = defineAgentloop(async ctx => {
if (!ctx.input) return;
const messages = ctx.transcript;
messages.push({
role: "user",
content: [{ type: "text", text: ctx.input.message }, ...(ctx.input.media ?? [])],
});
await ctx.setTranscript(messages);
const reply = await ctx.model({ messages, system: ctx.system, tools: [] });
messages.push(reply.message);
await ctx.setTranscript(messages);
});Build it
Install the builder:
npm install --save-dev esbuild@0.25.9 @bytecodealliance/componentize-js@0.19.3 @bytecodealliance/jco@1.17.9Save as build-loop.mjs:
import { build } from "esbuild";
import { componentize } from "@bytecodealliance/componentize-js";
import { writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const [entry = "loop.mjs", output = "loop.wasm"] = process.argv.slice(2);
const bundled = await build({
entryPoints: [entry], bundle: true, format: "esm", platform: "neutral", write: false,
external: ["brain:agentloop/host@0.2.0"],
});
const { component } = await componentize(bundled.outputFiles[0].text, {
witPath: fileURLToPath(import.meta.resolve("@aexhq/brain/contracts/agentloop.wit")),
worldName: "agentloop",
disableFeatures: ["stdio", "random", "clocks", "http", "fetch-event"],
});
await writeFile(output, component);Run node build-loop.mjs. The resulting loop.wasm is the file you give Brain.
For TypeScript, run node build-loop.mjs loop.ts; the builder bundles your source and the context adapter. The official loop build
shows TypeScript bundling with esbuild.
Use your built loop
In the quickstart, replace the Pi import with this declaration:
import { agentloop, brainEnv, component } from "@aexhq/brain";
const custom = agentloop({
implementation: component(new URL("./loop.wasm", import.meta.url)),
});Change agentloop: pi() to agentloop: custom({ env: brainEnv({ name: "brain" }) }),
remove the tools setting, and change the prompt to "Say hello in one sentence.".
Run npx tsx order.ts (or node order.mjs for JavaScript): the transcript contains your message and the model's reply.
Rebuild and create a new session to try each change; existing sessions keep their original loop.
A published factory can supply an Environment default, as Pi does, while letting callers
override it. Keep the underlying agentloop() declaration and Component object outside
the factory so repeated calls reuse the same admitted code.
Language choices
| Language | Working source | Build |
|---|---|---|
| JavaScript / TypeScript | The example above | node build-loop.mjs loop.mjs or node build-loop.mjs loop.ts |
| Rust | Reference loop below | Cargo with wasm32-wasip2 |
| Python | Minimal stateful loop below | tools/build-python-fixtures.sh |
| A language in your own service | HTTP loop example | Your environment's deployment tools |
The client SDK and the language of your loop are independent. These packaged examples produce
a .wasm file for Brain. A custom environment can instead run code in its own runtime.
Rust
The reference loop is a complete model-and-tool loop. Its model step reads and saves typed messages (excerpt):
let request = ModelRequest {
options: Default::default(),
messages: transcript.clone(),
system: None,
tools: None,
response_format: None,
max_output_tokens: None,
};
let result: ModelResult = decode(&brain::agentloop::host::model(&encode(&request)?)?)?;
transcript.push(result.message);
brain::agentloop::host::set_transcript(&encode(&transcript)?)?;Use the complete linked project to build. Clone Brain, edit
examples/reference-agentloop/src/lib.rs, and run from that checkout with Rust 1.97 or newer:
rustup target add wasm32-wasip2
cargo build --manifest-path examples/reference-agentloop/Cargo.toml --target wasm32-wasip2 --releaseCopy examples/reference-agentloop/target/wasm32-wasip2/release/reference_agentloop.wasm
to your app as loop.wasm, then use the session example above. This reference also supports
tools; restore the quickstart's tools setting to try its order lookup.
Python
This minimal loop saves a counter and emits an event on each activation. It demonstrates state and events; it does not call a model or use tools.
import json
from wit_world import WitWorld
from wit_world.imports import host
from wit_world.imports.types import TurnOutput
class WitWorld(WitWorld):
def turn(self, input):
kv = json.loads(input.kv_json)
calls = kv.get("calls", 0) + 1
host.kv_put("calls", json.dumps(calls))
sequence = host.emit("python_ran", json.dumps({"calls": calls}))
return TurnOutput(json.dumps({"sequence": sequence, "calls": calls}))Edit tests/fixtures/python/agentloop.py in a Brain checkout. With Python 3, venv and Bash (Linux or WSL), build:
bash tools/build-python-fixtures.sh "$PWD/build/python"Copy build/python/python-agentloop.wasm to your app as loop.wasm. Add
BRAIN_MAX_CORE_INSTANCES=1024 to a Brain server you control, then create a session with
custom as above. After sending a message, read session.events() to see python_ran and
the saved count. This example supplies computation and Brain callbacks, not Python file or network APIs.
Add your own behavior
Use the loop API for model calls, tool dispatch
and saved state. Save conversation changes explicitly; local variables do not survive the next
activation. Successful return completes the delivered event batch, including deliberately ignored
events. Use ctx.kv.get/set/delete for any progress or skipped observations your loop needs to remember.
For a complete JavaScript loop with tool dispatch and context management, use Pi's source as a starting point. Test your policy with fake model/tool responses, then run it against Brain. Check tool failures, cancellation and a second message, as well as the first successful answer.