Base agent

3. Write tool

View this stage's code on GitHub →

Same idea as Read, mirrored for writing: one more entry in the tools list, one more branch in the dispatcher. Adding a second tool before building the loop is deliberate — it proves the tool-calling mechanism generalizes before anything more complex sits on top of it.

Your task

  1. Add a Write tool: required file_path and content strings.
  2. Dispatch on the tool call's name — whichever of Read/Write the model asks for.

The solution

Try the task above first. When you want to compare, this is exactly what changed since stage 2 (just diff 2 3 shows the same).

Show the solutionHide the solution +39 −9 lines
main.py+39 −9
⋯ """Stage 2: the Read toolStage 3: the Write tool 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).Same pattern as Read: advertise the spec, dispatch by name when the modelcalls it. Still one round trip, still no loop — just a second tool.""" import argparse⋯ TOOLS = [                },            },        },    }    },    {        "type": "function",        "function": {            "name": "Write",            "description": "Write content to a file, creating it if needed or overwriting it if it exists",            "parameters": {                "type": "object",                "required": ["file_path", "content"],                "properties": {                    "file_path": {                        "type": "string",                        "description": "The path of the file to write to",                    },                    "content": {                        "type": "string",                        "description": "The content to write to the file",                    },                },            },        },    },]  ⋯ def Read(file_path):        return f.read()  def Write(file_path, content):    with open(file_path, "w") as f:        f.write(content)    return f"Wrote to {file_path}"  def main():    parser = argparse.ArgumentParser(description="petite-harness: a tiny AI coding assistant")    parser.add_argument("-p", "--prompt", required=True, help="the task to ask the model")⋯ def main():        call = message.tool_calls[0]        print(f"[main] model requested tool call: {call.function.name}", file=sys.stderr)         if call.function.name != "Read":        arguments = json.loads(call.function.arguments)         if call.function.name == "Read":            result = Read(arguments["file_path"])        elif call.function.name == "Write":            result = Write(arguments["file_path"], arguments["content"])        else:            raise RuntimeError(f"unknown tool: {call.function.name}")         arguments = json.loads(call.function.arguments)        print(Read(arguments["file_path"]))        print(result)        return     print(message.content)

Try it

Run this stage's own code:

just stage 3
cd .stages/03
just run "Create a file named hello.txt with exactly one line: Hello world"
cat hello.txt

Expect: Hello world

.stages/03 is a git worktree: your checkout stays on main, so just keeps working there. Compare with the previous stage with just diff 2 3, 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.

References

← 2. Read tool 4. Agent loop →