Showcase / Example
A first agent in 23 lines, and a recording of it at work
An agent is a model that uses tools in a loop until its task is done. The command affordance init writes the smallest useful one: an agent that can run shell commands and then give an answer. This page walks through that file and shows a real run of it.
The run
We gave the agent a task: count the lines in a file. Below is everything that happened, taken from the run's record. The grey italic text is the model thinking. Each blue label is a tool the model chose to call, and the green text is what came back.
23 ./app.tsx
00000000: 0a . 23 lines by awk
A recorded run against a DeepSeek-V4.1-Flash variant served by vLLM: 3 model calls and 1.1 seconds of generation.
The model ran one command, was not sure it could trust the count, ran a second command to check, and then answered. Nobody wrote that plan. The loop gave the model the tools and kept asking until it called answer.
The file
This is the whole agent. The numbered marks are explained under it.
// A first agent: it reads its task, may run shell commands, and stops when it answers.
// The page is the task, the tools and the log of what the model saw and did; ModelContext
// renders it in the bound model's format (a hosted chat API, or a native token-id server).
import { ModelContext } from "@affordance/lib/model.tsx";
import { useLog } from "@affordance/lib/log.tsx";
import { useLoop } from "@affordance/lib/loop.tsx";
import { Tool } from "@affordance/lib/tool.tsx";
import { machineTools } from "@affordance/tools/machine.tsx";
export default function Main({ task }) {
const log = useLog(); 1
useLoop(log); // ask the model, run its calls, repeat 2
const { shell } = machineTools(harness.machine.get("home"));
/** Give your final answer. This ends the task. */
function answer({ text }: { text: string }) { 3
harness.stop({ answer: text });
return "done";
}
return <ModelContext strategy="unrolled" log={[{ kind: "input", content: task }, ...log.records]}> 4
<Tool fn={shell}>{"[shell] "}</Tool>
<Tool fn={answer}>{"[answer]\n"}</Tool>
</ModelContext>;
}- 1The log. A list of everything so far: the task, each reply from the model, and the result of each tool.
- 2The loop. It sends the model the page below, runs whichever tool the model calls, adds the result to the log, and repeats.
- 3A tool. A tool is a function. The model is told its name, the comment above it and the type of its argument. This one ends the run.
- 4The page. What the model reads on each step: the task, the log and the two tools.
ModelContextwrites it in the format of whichever model you use.
Try it
You need a model for it to talk to. A model running on your own machine with llama.cpp, Ollama, vLLM or SGLang works, and so does a hosted API. You set that in affordance.yaml, next to the app. Getting started has the details.
Everything the agent hears back is written to a file as it goes. If you stop the run, affordance run --resume carries on from where it stopped.