Middleware & the agent loop¶
Middleware¶
Middleware wraps every call() in onion order (first registered is outermost):
import type { Middleware } from 'agent-toolbelt';
const timing: Middleware = async (ctx, next) => {
const start = Date.now();
try {
return await next();
} finally {
console.error(`${ctx.name} took ${Date.now() - start}ms`);
}
};
registry.use(timing);
ctx is { name, input, meta, tool }. input is already validated.
Built-in middleware¶
logging(logger?)— logs each call's name, duration and success/failure.requireScopes(getGranted)— enforces each tool's declaredscopesagainst the scopes the caller grants viameta."*"grants everything. Pairs with agent-passport:
registry.use(requireScopes((meta) => (meta.scopes as string[]) ?? []));
await registry.call('send_email', args, { scopes: passport.agent.scopes });
The agent loop¶
agent-toolbelt is the tool layer; you own the model loop:
const tools = registry.toJSONSchema();
let messages = [/* system + user */];
while (true) {
const res = await model.chat({ messages, tools }); // your LLM SDK
if (!res.toolCalls?.length) return res.text;
for (const tc of res.toolCalls) {
let output;
try {
output = await registry.call(tc.name, tc.arguments, { scopes });
} catch (err) {
output = { error: (err as Error).message }; // feed errors back to the model
}
messages.push(toolResultMessage(tc.id, output));
}
}
Because validation and authorization live in call(), the loop stays the same no matter
which model or SDK you use.