Most agent tutorials start with “set up your OpenAI API key” or “configure your AWS credentials.” If you want to understand how agents work, how a model decides to call a tool, uses the result, and reasons toward a final answer, there’s a setup tax before you write a single line of code.
You don’t need a cloud account or an API key to build your first agent. Ollama runs open-source LLMs locally on your machine, and the Strands Agents SDK from AWS gives you a proper agent loop with tool calling built in. Together, you can build a fully functional agent on your laptop at zero cost. This post walks through the entire setup from scratch.
Install Ollama and Pull a Model
Ollama is a local large language model runtime that downloads open-source models and serves them over an HTTP API. Think of it as a lightweight server for running LLM on your own hardware.
Install it:
curl -fsSL https://ollama.com/install.sh | shOnce installed, pull the model we’ll use throughout this post:
ollama pull qwen3.5:9bThis downloads qwen3.5:9b, a 9 billion parameter model from Alibaba with a nice 256K token context window. The default download is the Q4_K_M quantization at 6.6 GB, which runs comfortably on most consumer hardware with 8 GB of RAM or more. It also supports tool calling natively, which is what we need.
Verify it works by starting an interactive session:
ollama run qwen3.5:9bAsk it something, confirm you get a response, then type /bye to exit. You won’t need the interactive mode again.
You’ll probably notice that the response is not quite as snappy is it is when using a frontier model like Claude or ChatGPT, but thats the tradeoff we get for running our own model locally in that we’re limited by our own hardware.
For our agent to be able to connect to our Ollama model, we’ll run the background local API service that Ollama provides which runs at http://localhost:11343. To start it, simply run:
ollama serveNote: Any model tagged with
toolson ollama.com/search will work with this tutorial.qwen3.5:9bis a solid default because it handles tool calling reliably at its size (my computer about died when I tried to run the 142GBqwen3:235bmodel).
Set Up the Python Project
Install uv if you don’t have it already:
curl -LsSf https://astral.sh/uv/install.sh | shCreate the project and add the dependencies:
uv init my-first-agentcd my-first-agentuv add "strands-agents[ollama]" strands-agents-toolsuv init creates a pyproject.toml, a starter main.py, and a .python-version file. uv add installs the dependencies, creates a .venv, and generates a uv.lock lockfile, all in one step. No manual pip install or source .venv/bin/activate needed.
The two packages we’re installing:
strands-agents[ollama]is the Strands Agents SDK with the Ollama model provider included.strands-agents-toolsis a library of pre-built tools we’ll use in a later step.
Strands Agents is an open-source agent framework from AWS. It handles the agent loop: sending a prompt to a model, executing whatever tool calls the model generates, feeding those results back, and repeating until the model produces a final response. You define what your agent can do; Strands handles the orchestration.
Setting up Strands with Ollama
Create agent.py in the project root and add the following:
from strands import Agentfrom strands.models.ollama import OllamaModel
model = OllamaModel( host="http://localhost:11434", model_id="qwen3.5:9b",)
agent = Agent(model=model)agent("What is the capital of Japan?")Run it:
uv run agent.pyWalking through each piece:
OllamaModelis the Strands provider that talks directly to your local Ollama server. No API keys, no cloud setup needed!hostis where Ollama is running (localhost:11434is the default).model_idmatches the model name you pulled withollama pull.Agent(model=model)creates an agent backed by that model.agent("...")sends a prompt through the agent loop and prints the response to stdout.
This is an agent with no tools, so it can only respond with what the model already knows. Let’s now make our agent actually useful by giving it a tool that it can call.
Building your first Tool
Tools are simply functions your agent can decide to call. The model sees each tool’s name, description, and parameter schema. When it determines a tool is relevant to the user’s request, it generates a structured call. You don’t write logic to decide when tools run, that’s the model’s job based on the system prompt and session inputs you give it. The agent will reason about the inputs and determine which tools to call to fulfill the user’s request.
Let’s add a calculator tool to our agent.py:
from strands import Agent, toolfrom strands.models.ollama import OllamaModel
@tooldef calculator(operation: str, a: float, b: float) -> str: """Perform a math operation on two numbers.
Args: operation: The operation to perform. One of: add, subtract, multiply, divide. a: The first number. b: The second number. """ match operation: case "add": return f"{a} + {b} = {a + b}" case "subtract": return f"{a} - {b} = {a - b}" case "multiply": return f"{a} * {b} = {a * b}" case "divide": if b == 0: return "Cannot divide by zero" return f"{a} / {b} = {a / b}" case _: return f"Unknown operation: {operation}"
model = OllamaModel( host="http://localhost:11434", model_id="qwen3.5:9b",)
agent = Agent(model=model, tools=[calculator])agent("What is 42 multiplied by 17, and then subtract 100 from that result?")A few things worth noting about the @tool decorator:
- The docstring is what the model sees as the tool’s description. Clear, specific descriptions help the model know when to use a tool and how to populate its parameters correctly.
- Type hints on the parameters define the input schema that gets sent to the model alongside the description.
- The return value is sent back to the model as a tool result message, which the model folds into its response.
Run it again with uv run agent.py. You should see the agent call the calculator twice: once to multiply 42 by 17, and again to subtract 100 from the result. Then it produces a natural language answer incorporating both.
Adding a Second Tool
The real value of agents comes from combining multiple tools, and letting the model determine which ones to call and in what order.
strands-agents-tools includes a set of pre-built tools you can drop straight into an agent. Pull in current_time and add it alongside the calculator:
from strands import Agent, toolfrom strands.models.ollama import OllamaModelfrom strands_tools import current_time
@tooldef calculator(operation: str, a: float, b: float) -> str: """Perform a math operation on two numbers.
Args: operation: The operation to perform. One of: add, subtract, multiply, divide. a: The first number. b: The second number. """ match operation: case "add": return f"{a} + {b} = {a + b}" case "subtract": return f"{a} - {b} = {a - b}" case "multiply": return f"{a} * {b} = {a * b}" case "divide": if b == 0: return "Cannot divide by zero" return f"{a} / {b} = {a / b}" case _: return f"Unknown operation: {operation}"
model = OllamaModel( host="http://localhost:11434", model_id="qwen3.5:9b",)
agent = Agent(model=model, tools=[calculator, current_time])agent("What time is it right now, and what is 365 multiplied by 24?")The agent will call current_time to get the real time from your system and calculator to do the multiplication. Try rephrasing the prompt in different ways. The model should figure out which tools apply regardless of how you ask.
What’s Happening Under the Hood
The agent loop that Strands runs looks like this:
- Your prompt goes to the model along with descriptions of all available tools.
- The model either generates a direct response or produces one or more structured tool calls.
- Strands executes those tool calls and appends the results to the conversation as new messages.
- The model gets another turn. It can call more tools or produce a final response.
- This repeats until the model produces a response with no tool calls.
This is what distinguishes an agent from a plain chat completion, it is the Agentic Loop. The model isn’t just generating text in one pass. It’s deciding which capabilities to use, executing actions against real data, and building its final answer from those results.
One thing that matters here is model choice. Tool calling requires the model to output structured JSON in a specific format at the right moment in a conversation. Not every model does this reliably which is why we’re using qwen3.5:9b, a model that was explicitly trained to use tools.
Where to Go from Here
Now you’ve got a local agent development environment that costs nothing to run and doesn’t require any cloud setup. From here, the interesting work is building tools. Try writing a tool that reads a file, fetches a URL, or queries a local database. Anything your agent should be able to act on is a candidate for a tool. The @tool decorator keeps the surface area small.
Strands goes deeper than what this post covers. The documentation has good guides on conversation memory, hooks for validating or intercepting tool calls, multi-agent patterns, and MCP tool servers, which let you connect to any MCP-compatible tool provider without writing the tool yourself. The community tools package also has a growing set of ready-made tools beyond what we used here.
When you’re ready to move beyond local models, swapping providers is a one-line change. Strands supports Amazon Bedrock, OpenAI, Google, and others. The same agent code works across all of them, which makes local development with Ollama a natural first step.