Aex Brain
Customize and operate

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.9

Save 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

LanguageWorking sourceBuild
JavaScript / TypeScriptThe example abovenode build-loop.mjs loop.mjs or node build-loop.mjs loop.ts
RustReference loop belowCargo with wasm32-wasip2
PythonMinimal stateful loop belowtools/build-python-fixtures.sh
A language in your own serviceHTTP loop exampleYour 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 --release

Copy 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.

On this page