Chapter 8 — Human in the Loop¶
Setup Instructions¶
To ensure you have the required dependencies to run this notebook, you'll need to have our llm-agents-from-scratch framework installed on the running Jupyter kernel. To do this, you can launch this notebook with the following command while within the project's root directory:
uv run --with jupyter jupyter lab
Alternatively, if you just want to use the published version of llm-agents-from-scratch without local development, you can install it from PyPi by uncommenting the cell below.
# Uncomment the line below to install `llm-agents-from-scratch` from PyPi
# !pip install llm-agents-from-scratch
Running an Ollama service¶
To execute the code provided in this notebook, you'll need to have Ollama installed on your local machine and have its LLM hosting service running. To download Ollama, follow the instructions found on this page: https://ollama.com/download. After downloading and installing Ollama, you can start a service by opening a terminal and running the command ollama serve.
import os, shutil, subprocess, time, urllib.request, urllib.error
def ensure_ollama(host="http://localhost:11434", timeout=15):
"""Start Ollama if not already running and wait until responsive."""
def _up():
try:
urllib.request.urlopen(f"{host}/api/tags", timeout=1)
return True
except (urllib.error.URLError, ConnectionError, TimeoutError):
return False
if _up():
return print(f"✓ Ollama already running at {host}")
# Lightning persistent path first, then standard locations
ollama_path = shutil.which("ollama")
if ollama_path is None:
for candidate in [
"/teamspace/studios/this_studio/.local/bin/ollama",
"/usr/local/bin/ollama",
"/usr/bin/ollama",
]:
if os.path.exists(candidate):
ollama_path = candidate
break
if ollama_path is None:
raise RuntimeError(
"Could not find the ollama binary. Install with: "
"curl -fsSL https://ollama.com/install.sh | sh"
)
print(f"Starting Ollama server ({ollama_path})...")
subprocess.Popen(
[ollama_path, "serve"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
deadline = time.time() + timeout
while time.time() < deadline:
if _up():
return print(f"✓ Ollama up and running at {host}")
time.sleep(0.5)
raise RuntimeError(f"Ollama did not start within {timeout}s")
use_cloud = "OLLAMA_API_KEY" in os.environ
ensure_ollama() if not use_cloud else print("✓ Using Ollama Cloud")
✓ Using Ollama Cloud
Examples¶
Example 1: Agent-Initiated Pause with HumanInputTool¶
import logging
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.logger import enable_console_logging
from llm_agents_from_scratch.llms import OllamaLLM
from llm_agents_from_scratch.tools import SimpleFunctionTool
from llm_agents_from_scratch.tools.default import HumanInputTool
enable_console_logging(logging.INFO)
def next_number(x: int) -> int:
if x % 2 == 0:
return x // 2
return 3 * x + 1
model = "qwen3.5:397b-cloud" if use_cloud else "qwen3:14b"
host = "https://ollama.com" if use_cloud else None
next_number_tool = SimpleFunctionTool(func=next_number)
human_input_tool = HumanInputTool()
llm = OllamaLLM(host=host, model=model, think=False, json_prompt_mode=use_cloud)
agent = LLMAgent(llm=llm, tools=[next_number_tool, human_input_tool])
task = Task(
instruction=(
"Compute the full Hailstone sequence step by step using next_number. "
"The starting number must come from the human operator — ask them "
"before beginning."
)
)
result = await agent.run(task)
print(result.content)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Compute the full Hailstone sequence step by step using next_number. The starting number must come from the human operator — ask them ...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compute the full Hailstone sequence step by step using next_number. The starting number must come from the human operator — ask th...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__human_input
╭────────────────────────────────────────────────── Human Input ──────────────────────────────────────────────────╮ │ What starting number should I use for the Hailstone sequence? │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>:
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 4
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "26ae513d-d0f6-49cd-a257-648a88d152ab",
"tool_name": "from_scratch__use_skill...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to use the 'stop-at-one' skill with the provided ID and arguments.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to use the 'stop-at-one' skill with the provided ID and arguments.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_skill
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: <skill_content name="stop-at-one">
# Stop At One
Compute a full sequence from a starting number down to 1
using the `next_num...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "0ccd40d6-dc42-4812-828d-84021196cf7f",
"tool_name": "from_scratch__next_numb...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to compute the next number in the Hailstone sequence using the 'from_scratch__next_number' tool with argument x=4.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to compute the next number in the Hailstone sequence using the 'from_scratch__next_number' tool with argumen...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 2
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "5beba308-d49f-409f-9ed9-f72348556539",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to compute the next number in the Hailstone sequence using the 'next_number' tool with argument x=2.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to compute the next number in the Hailstone sequence using the 'next_number' tool with argument x=2.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 1
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace through what we have:
- Starting number...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace through what we have:
- Starting num...[TRUNCATED]
The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace through what we have:
- Starting number: 4
- Step 1: next_number(4) → 2
- Step 2: next_number(2) → 1
The complete sequence is: 4 → 2 → 1
Now I need to report the results as specified in the skill instructions:
- **Starting number**: 4
- **Total steps taken**: 2 (we made 2 tool calls)
- **Maximum value reached**: 4 (the starting number was the highest)
The full Hailstone sequence from 4 down to 1 is: **4 → 2 → 1**
Example 2: End-of-Loop Approval Gate¶
import logging
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.logger import enable_console_logging
from llm_agents_from_scratch.llms import OllamaLLM
from llm_agents_from_scratch.tools import SimpleFunctionTool
enable_console_logging(logging.INFO)
def next_number(x: int) -> int:
if x % 2 == 0:
return x // 2
return 3 * x + 1
model = "qwen3.5:397b-cloud" if use_cloud else "qwen3:14b"
host = "https://ollama.com" if use_cloud else None
next_number_tool = SimpleFunctionTool(func=next_number)
llm = OllamaLLM(host=host, model=model, think=False, json_prompt_mode=use_cloud)
agent = LLMAgent(llm=llm, tools=[next_number_tool])
task = Task(
instruction=(
"Compute the full Hailstone sequence starting from 5, "
"step by step using next_number, until you reach 1."
)
)
result = await agent.run(task, with_approval=True)
print(result.content)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Compute the full Hailstone sequence starting from 5, step by step using next_number, until you reach 1.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compute the full Hailstone sequence starting from 5, step by step using next_number, until you reach 1.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 16
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "c3d2ebae-42fc-49f0-bb2b-56fca08abeb6",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call with id 'c3d2ebae-42fc-49f0-bb2b-56fca08abeb6' using the tool 'next_number' and argument x=16.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call with id 'c3d2ebae-42fc-49f0-bb2b-56fca08abeb6' using the tool 'next_number' and argument x=16.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 8
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "54b901af-bc59-49d8-a838-8edfdf998a96",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call with id '54b901af-bc59-49d8-a838-8edfdf998a96' using the tool 'next_number' and argument x=8.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call with id '54b901af-bc59-49d8-a838-8edfdf998a96' using the tool 'next_number' and argument x=8.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 4
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "05de6699-0429-42fe-9906-5b93207fb395",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call with id '05de6699-0429-42fe-9906-5b93207fb395' using the tool 'next_number' and argument x=4.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call with id '05de6699-0429-42fe-9906-5b93207fb395' using the tool 'next_number' and argument x=4.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 2
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "69cc3192-4b9a-4653-b732-8d4cbda0d5e5",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call with id '69cc3192-4b9a-4653-b732-8d4cbda0d5e5' using the tool 'next_number' and argument x=2.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call with id '69cc3192-4b9a-4653-b732-8d4cbda0d5e5' using the tool 'next_number' and argument x=2.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 1
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace back the full sequence:
- Starting from...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
╭───────────────────────────────────────────── Proposed Task Result ──────────────────────────────────────────────╮ │ The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace back the full │ │ sequence: │ │ │ │ - Starting from 5 │ │ - next_number(5) = 16 │ │ - next_number(16) = 8 │ │ - next_number(8) = 4 │ │ - next_number(4) = 2 │ │ - next_number(2) = 1 │ │ │ │ The complete Hailstone sequence starting from 5 is: 5 → 16 → 8 → 4 → 2 → 1 │ │ │ │ We have successfully computed the full Hailstone sequence until reaching 1. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Approve this result? [y/n]:
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace back the full sequence: - Starting f...[TRUNCATED] The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace back the full sequence: - Starting from 5 - next_number(5) = 16 - next_number(16) = 8 - next_number(8) = 4 - next_number(4) = 2 - next_number(2) = 1 The complete Hailstone sequence starting from 5 is: 5 → 16 → 8 → 4 → 2 → 1 We have successfully computed the full Hailstone sequence until reaching 1.
Example 3: Caller-Driven Stepwise Execution with run_supervised()¶
import logging
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.logger import enable_console_logging
from llm_agents_from_scratch.llms import OllamaLLM
from llm_agents_from_scratch.tools import SimpleFunctionTool
enable_console_logging(logging.INFO)
def next_number(x: int) -> int:
if x % 2 == 0:
return x // 2
return 3 * x + 1
model = "qwen3.5:397b-cloud" if use_cloud else "qwen3:14b"
host = "https://ollama.com" if use_cloud else None
next_number_tool = SimpleFunctionTool(func=next_number)
llm = OllamaLLM(host=host, model=model, think=False, json_prompt_mode=use_cloud)
agent = LLMAgent(llm=llm, tools=[next_number_tool])
task = Task(
instruction=(
"Compute the full Hailstone sequence starting from 4, "
"step by step using next_number, until you reach 1."
)
)
task_handler = await agent.run_supervised(task, explicit_only_skills=["stop-at-one"])
step = await task_handler.get_next_step(None)
step
TaskStep(id_='59d5aff2-1b3d-4e4b-9fb5-e2f830168f3c', task_id='5dd0f1dd-b259-4138-b515-4efd353be772', instruction='Compute the full Hailstone sequence starting from 4, step by step using next_number, until you reach 1.')
step_result = await task_handler.run_step(step)
step_result
INFO (llm_agents_fs.SupervisedTaskHandler) : ⚙️ Processing Step: Compute the full Hailstone sequence starting from 4, step by step using next_number, until you reach 1.
INFO (llm_agents_fs.SupervisedTaskHandler) : 🛠️ Executing Tool Call: next_number
INFO (llm_agents_fs.SupervisedTaskHandler) : ✅ Successful Tool Call: 2
INFO (llm_agents_fs.SupervisedTaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "31db3c2f-19c0-4b62-b159-c2f0c9e23a10",
"tool_name": "next_number",
"argu...[TRUNCATED]
TaskStepResult(task_step_id='59d5aff2-1b3d-4e4b-9fb5-e2f830168f3c', content='I need to make the following tool-calls:\n{\n "id_": "31db3c2f-19c0-4b62-b159-c2f0c9e23a10",\n "tool_name": "next_number",\n "arguments": {\n "x": "2"\n }\n}')
step = await task_handler.get_next_step(step_result)
step
INFO (llm_agents_fs.SupervisedTaskHandler) : 🧠 New Step: Execute the tool call with id '31db3c2f-19c0-4b62-b159-c2f0c9e23a10' to compute the next number in the Hailstone sequence for x=2.
TaskStep(id_='5a99143b-94af-48ed-9d7c-4149f7864725', task_id='5dd0f1dd-b259-4138-b515-4efd353be772', instruction="Execute the tool call with id '31db3c2f-19c0-4b62-b159-c2f0c9e23a10' to compute the next number in the Hailstone sequence for x=2.")
step_result = await task_handler.run_step(step)
step_result
INFO (llm_agents_fs.SupervisedTaskHandler) : ⚙️ Processing Step: Execute the tool call with id '31db3c2f-19c0-4b62-b159-c2f0c9e23a10' to compute the next number in the Hailstone sequence for x=2. INFO (llm_agents_fs.SupervisedTaskHandler) : 🛠️ Executing Tool Call: next_number INFO (llm_agents_fs.SupervisedTaskHandler) : ✅ Successful Tool Call: 1 INFO (llm_agents_fs.SupervisedTaskHandler) : ✅ Step Result: The tool returned 1, which means the next number in the Hailstone sequence after 2 is 1. Let me trace the full sequence I've computed ...[TRUNCATED]
TaskStepResult(task_step_id='5a99143b-94af-48ed-9d7c-4149f7864725', content="The tool returned 1, which means the next number in the Hailstone sequence after 2 is 1.\n\nLet me trace the full sequence I've computed so far:\n- Starting from 4\n- next_number(4) = 2\n- next_number(2) = 1\n\nSince I've reached 1, the Hailstone sequence is complete. The full sequence is: 4 → 2 → 1\n\nThe task asked me to compute the full Hailstone sequence starting from 4 until reaching 1, and I have successfully done that. The sequence is: **4, 2, 1**.")
step = await task_handler.get_next_step(step_result)
step
INFO (llm_agents_fs.SupervisedTaskHandler) : No new step required.
TaskResult(task_id='5dd0f1dd-b259-4138-b515-4efd353be772', content="The tool returned 1, which means the next number in the Hailstone sequence after 2 is 1.\n\nLet me trace the full sequence I've computed so far:\n- Starting from 4\n- next_number(4) = 2\n- next_number(2) = 1\n\nSince I've reached 1, the Hailstone sequence is complete. The full sequence is: 4 → 2 → 1\n\nThe task asked me to compute the full Hailstone sequence starting from 4 until reaching 1, and I have successfully done that. The sequence is: **4, 2, 1**.")
# since result is a TaskResult, we can call complete()
await task_handler.complete(step)
print(result.content)
The tool returned 1, which means we've reached the end of the Hailstone sequence. Let me trace back the full sequence: - Starting from 5 - next_number(5) = 16 - next_number(16) = 8 - next_number(8) = 4 - next_number(4) = 2 - next_number(2) = 1 The complete Hailstone sequence starting from 5 is: 5 → 16 → 8 → 4 → 2 → 1 We have successfully computed the full Hailstone sequence until reaching 1.
Note — Agent Inspector
The agent above can also be driven step-by-step through a browser UI using the Agent Inspector (
llm-agents-from-scratch-inspector). Install it withpip install llm-agents-from-scratch-inspector, then create a launch script that exposes anagent_builderat module scope:
# main.py
from llm_agents_from_scratch import LLMAgentBuilder
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.llms import OllamaLLM
from llm_agents_from_scratch.tools import SimpleFunctionTool
def next_number(x: int) -> int:
if x % 2 == 0:
return x // 2
return 3 * x + 1
agent_builder = (
LLMAgentBuilder()
.with_llm(OllamaLLM(model="qwen3:14b"))
.with_tool(SimpleFunctionTool(func=next_number))
)
default_task = Task(
instruction=(
"Compute the full Hailstone sequence starting from 4, "
"step by step using next_number, until you reach 1."
)
)
Then launch:
agent-inspector launch main.pyThis requires an Ollama server running locally (
ollama serve). Alternatively, point the agent at an Ollama cloud model by settingOLLAMA_API_KEYin your environment and passinghost="https://ollama.com"andjson_prompt_mode=TruetoOllamaLLM— the Inspector works the same either way.A browser tab opens where you can enter a task, create a session, and step through
get_next_step()/run_step()interactively. See the Inspector repository for full usage instructions.