Base agent
2. Read tool
View this stage's code on GitHub →An LLM can't touch a filesystem by default — it only produces text. A "tool" is just a JSON schema describing a function, sent alongside the prompt, that the model can ask to have called. This stage advertises exactly one: Read. Still a single round trip — the model asks, you call it, you print the result and exit. No loop yet, because the point here is purely "the model can ask for something it doesn't have."
Your task
- Add a
Readtool to the request'stoolsarray: a JSON schema with one requiredfile_pathstring. - When the model's response includes a tool call for it, execute it yourself (read the file) and print the raw contents. Still a single round trip — don't loop yet.
The solution
Try the task above first. When you want to compare, this is exactly what changed since stage 1 (just diff 1 2 shows the same).
Show the solutionHide the solution +46 −8 lines
main.py+46 −8
⋯ """Stage 1: Talk to an LLMStage 2: the Read tool The smallest possible version of an AI coding assistant: take a promptfrom the command line, send it to an LLM, print the response. No tools yet. No loop. Just a single request/response round trip, usingthe OpenAI SDK pointed at OpenRouter (any OpenAI-compatible API works thesame way).The model can't touch your filesystem on its own — it can only ask. Thisstage adds one tool, Read, and a single round trip: send the prompt andthe tool's spec, check if the model asked to call it, execute it if so,print the result (no loop yet, that's the next stage).""" import argparseimport jsonimport osimport sys ⋯ API_KEY = os.getenv("OPENROUTER_API_KEY")BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")MODEL = os.getenv("MODEL", "anthropic/claude-haiku-4.5") TOOLS = [ { "type": "function", "function": { "name": "Read", "description": "Read and return the contents of a file", "parameters": { "type": "object", "required": ["file_path"], "properties": { "file_path": { "type": "string", "description": "The path to the file to read", } }, }, }, }] def Read(file_path): with open(file_path) as f: return f.read() def main(): parser = argparse.ArgumentParser(description="petite-harness: a tiny AI coding assistant")⋯ def main(): response = client.chat.completions.create( model=MODEL, messages=[{"role": "user", "content": args.prompt}], tools=TOOLS, ) if not response.choices: raise RuntimeError("no choices in response") print(response.choices[0].message.content) message = response.choices[0].message if message.tool_calls: call = message.tool_calls[0] print(f"[main] model requested tool call: {call.function.name}", file=sys.stderr) if call.function.name != "Read": raise RuntimeError(f"unknown tool: {call.function.name}") arguments = json.loads(call.function.arguments) print(Read(arguments["file_path"])) return print(message.content) if __name__ == "__main__":
Try it
Run this stage's own code:
just stage 2
cd .stages/02
echo "hello from petite" > test.txt
just run "Show me what's inside test.txt. Respond with only the file contents, no backticks."
Expect: hello from petite
.stages/02 is a git worktree: your checkout stays on main, so
just keeps working there. Compare with the previous stage with just diff 1 2,
or browse it on GitHub;
just clean-stages removes the worktrees.
Where this fits
The whole agent; this stage builds the highlighted part. Click any part to jump to its stage.