Level 1
What is an agent?
- user
- assistant
- tool_result
EventBus
The problem
A chatbot, a script that calls a model twice, and a system that fixes bugs on its own all get called agents. That makes it hard to know what you're building, and harder to pick the right tool for the job.
The animation is a little adventure game. A villager asks: "I lost the key to the village chest. Where is it?".
The game has two functions that can help: ask_villager asks someone what they saw, and
search_area searches one spot on the map. Watch who decides which of them runs, and when.
What makes it an agent
An agent isn't "an LLM with tools" and it isn't "a smart workflow". It's a system where the model chooses the control flow: which tool to call, in what order, and when to stop. Your code never says "ask the fisher, then search the oak". The model reads each result and decides the next step.
The loop that makes that possible is the next level.
In the animation
- The villager your app
- The question starts at a house in the village, and the answer comes back there.
- Astor the loop
- The agent loop, with the bandoneón of messages on his back. He goes wherever the Oracle's notes send him, and nowhere else.
- The Oracle LLM
- The model, in a cave between two fires. It never leaves: it only knows what's in the bandoneón. Without tools it can only talk, like Petrus the Inert.
- The dock and the woods tools
-
ask_villagerandsearch_area: your own functions. A screen stays dark until a path leads to it. - The lit screens control flow
- The whole pattern in one picture, also on the mini-map. Each path appears only when the Oracle asks for that tool, after reading the last result. In a workflow, your code would have drawn them all before anyone asked. The mountain stays dark: the model never needed it.
- Rupees and hearts tokens, maxTurns
- Every visit to the Oracle costs rupees, because the whole bandoneón is read again, and a heart from the turn budget. The numbers are illustrative.
The code
With astorlm: Wrap your own functions with tool() and hand them to the agent. The loop is built in: the model
picks which tools to call, in what order, and when it has enough to answer.
From scratch: Your game's functions, handed to the model as tools. The loop itself is the one from level 2; the only new thing is which tools it gets.
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'
// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const provider = new OpenAIProvider({
baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
apiKey: 'YOUR_API_KEY', // local servers usually ignore it
})
// Your own game functions, wrapped as tools: a name, a description and an input schema.
const askVillager = tool({
name: 'ask_villager',
description: 'Ask someone in the village what they saw. Returns what they say.',
schema: z.object({ name: z.string().describe('Who to ask, e.g. "fisher" or "baker"') }),
execute: async ({ name }) => world.villager(name).say(),
})
const searchArea = tool({
name: 'search_area',
description: 'Search one spot on the map. Returns what is found there, if anything.',
schema: z.object({ area: z.string().describe('A named spot, e.g. "old oak" or "bridge"') }),
execute: async ({ area }) => world.search(area),
})
// The model decides which tools to call, in what order, and when to stop.
const agent = await createLocalAgent({
provider,
tools: [askVillager, searchArea],
maxTurns: 5, // a cap on the laps, in case it never settles
})
await agent.run('I lost the key to the village chest. Where is it?') // "In the crow's nest on the old oak"
// An agent that finds a villager's lost key. Plain TypeScript, no SDK.
// Your game. In a real one these read the world state and the characters' dialogue.
async function askVillager(name: string) {
const seen: Record<string, string> = {
fisher: 'A crow flew off with something shiny, toward the old oak in the woods.',
}
return seen[name] ?? `The ${name} saw nothing.`
}
async function searchArea(area: string) {
return area === 'old oak' ? "In the crow's nest: a small brass key." : `Nothing at the ${area}.`
}
// The same functions, as tools the model can ask for. Each one returns text.
const tools = {
ask_villager: ({ name }: { name: string }) => askVillager(name),
search_area: ({ area }: { area: string }) => searchArea(area),
}
// runAgent is the loop from level 2, with the tools passed in. Your code never says
// "ask the fisher, then search the oak": the model picks each step after reading the last result.
export async function agent(question: string): Promise<string> {
return runAgent(question, tools)
}
# An agent that finds a villager's lost key. Standard library only, no SDK.
# Your game. In a real one these read the world state and the characters' dialogue.
def ask_villager(name):
seen = {"fisher": "A crow flew off with something shiny, toward the old oak in the woods."}
return seen.get(name, f"The {name} saw nothing.")
def search_area(area):
return "In the crow's nest: a small brass key." if area == "old oak" else f"Nothing at the {area}."
# The same functions, as tools the model can ask for. Each one returns text.
TOOLS = {
"ask_villager": lambda args: ask_villager(args["name"]),
"search_area": lambda args: search_area(args["area"]),
}
# run_agent is the loop from level 2, with the tools passed in. Your code never says
# "ask the fisher, then search the oak": the model picks each step after reading the last result.
def agent(question):
return run_agent(question, TOOLS)
When to use one, and what it costs
- Use one when you can't write the steps down in advance. The key could be in a nest, under a bridge, or already sold at the shop: in code, every new case is another branch. The agent handles them with the same loop, as long as it has the tools.
-
Every step is a model call. Two tools meant three calls here. More steps mean more latency and
more tokens, so cap the laps with
maxTurns. - The same question can take a different path. Log the path the model chose, so you can see why an answer came out the way it did.
- Tools are the boundary. The model can only do what your tools allow. What you hand it is what it can break.