User-Explicit Skill Activation — Hailstone¶
This notebook demonstrates user-explicit skill activation using agent.run_with_skill() with the Hailstone (stop-at-one) skill, and compares the execution trace against the model-driven activation shown in chapter-notebooks/ch06/ch06.ipynb.
| Activation mode | How the skill is triggered |
|---|---|
| Model-driven | The model reads the skill catalog and decides to call from_scratch__use_skill |
| User-explicit | The caller passes the skill name directly via run_with_skill() — the instruction is pre-framed |
Note: Run this notebook from within the more-examples/ch06/ directory so the stop-at-one skill at .agents/skills/stop-at-one/ is discovered as a project-scoped skill.
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.
Setting up the LLM¶
I recommend running these notebooks on Ollama Cloud. It serves much larger models than most laptops can run. The default cloud model is picked by benchmarking candidates on these notebooks, and an automated run checks every notebook against it each week. The notebook status page shows the current model and results.
- Create an API key at https://ollama.com/settings/keys.
- Export it before launching Jupyter:
export OLLAMA_API_KEY=<your key>. - That's all. When a notebook builds its LLM, it detects the key and uses Ollama Cloud.
Prefer to run locally? Install Ollama from https://ollama.com/download
and pull the model with ollama pull qwen3:14b. If no key is set, the
notebook starts the Ollama service for you, so there is no need to run
ollama serve. Local models are smaller, so your outputs will differ from
the recorded ones.
Prefer OpenAI or Anthropic?
- Install the extra:
uv sync --extra openaioruv sync --extra anthropic. - Export
OPENAI_API_KEYorANTHROPIC_API_KEYbefore launching Jupyter. If you forget, you will be prompted for it, which keeps the key out of the saved notebook. - Set
LLM_PROVIDER=openai(oranthropic), or passprovider="openai"tomake_llm(). A key on its own never switches providers, so one exported for unrelated work cannot reroute you.
The switch applies wherever a notebook builds its LLM with make_llm().
A notebook that constructs OllamaLLM directly stays on Ollama. Output on
gpt-5 or Claude will differ from the recorded outputs, and prompt-sensitive
examples (the ch09 evaluator pattern, ch08 supervised trajectories) may
behave noticeably differently.
# Uncomment the line below to install `llm-agents-from-scratch` from PyPI
# !pip install llm-agents-from-scratch
from llm_agents_from_scratch.notebook_utils import make_llm
Shared Setup¶
Both examples use the same agent: an LLMAgent with the next_number tool and the stop-at-one skill discovered automatically.
import logging
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.logger import enable_console_logging
from llm_agents_from_scratch.tools.simple_function import SimpleFunctionTool
enable_console_logging(logging.INFO)
def next_number(x: int) -> int:
"""Return the next number in the Collatz sequence."""
if x % 2 == 0:
return x // 2
return 3 * x + 1
next_number_tool = SimpleFunctionTool(func=next_number)
llm = make_llm()
agent = LLMAgent(llm=llm, tools=[next_number_tool])
✓ Using Ollama Cloud (kimi-k2.7-code:cloud)
Example 1: User-Explicit Activation¶
run_with_skill() pre-frames the instruction so the model's first action is always to activate the named skill — no catalog browsing required.
result_explicit = await agent.run_with_skill(
"stop-at-one",
prompt="Compute the stop-at-one sequence for 8.",
)
print(result_explicit)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: This is a user-explicit skill activation. Call the from_scratch__use_skill tool with name='stop-at-one'. Use exactly this name — it i...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: This is a user-explicit skill activation. Call the from_scratch__use_skill tool with name='stop-at-one'. Use exactly this name — i...[TRUNCATED]
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_": "1a3e46b9-2699-4121-a6f3-d6f81c585c9e",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the pending tool call: call next_number with x=8 and wait for the result before proceeding.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the pending tool call: call next_number with x=8 and wait for the result before proceeding.
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_": "024b963b-18f1-4d30-818f-079e2d1eece5",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the pending tool call: call next_number with x=4 and wait for the result before proceeding. Sequence so far: [8, 4].
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the pending tool call: call next_number with x=4 and wait for the result before proceeding. Sequence so far: [8, 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_": "b9cb0f96-f2e0-46cd-a86b-c8b310beac22",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the pending tool call: call next_number with x=2 and wait for the result before proceeding. Sequence so far: [8, 4, 2].
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the pending tool call: call next_number with x=2 and wait for the result before proceeding. Sequence so far: [8, 4, 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`, so the sequence is complete: `[8, 4, 2, 1]`.
Following the skill's Step 5, here is the final report:
- **Seque...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The tool returned `1`, so the sequence is complete: `[8, 4, 2, 1]`.
Following the skill's Step 5, here is the final report:
- **Se...[TRUNCATED]
The tool returned `1`, so the sequence is complete: `[8, 4, 2, 1]`.
Following the skill's Step 5, here is the final report:
- **Sequence:** 8 → 4 → 2 → 1
- **Starting number:** 8
- **Total steps taken (tool calls made):** 3
- **Maximum value reached:** 8
Example 2: Model-Driven Activation (for comparison)¶
With model-driven activation the agent receives the raw task instruction and must read the skill catalog, decide to call from_scratch__use_skill, and only then follow the skill instructions.
from llm_agents_from_scratch.data_structures import Task
task = Task(instruction="Compute the stop-at-one sequence for 8.")
result_model_driven = await agent.run(task)
print(result_model_driven)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Compute the stop-at-one sequence for 8.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compute the stop-at-one sequence for 8.
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_": "c0f6dce6-7ab2-4de0-ab50-b36e06e59cae",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the next_number tool call with x=8 and wait for the result.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the next_number tool call with x=8 and wait for the result.
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_": "70dc007b-fe84-464f-ba7e-e376b9f93bf9",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the next_number tool with x=4, wait for the result, append it to the sequence, and continue the stop-at-one loop until the result equ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the next_number tool with x=4, wait for the result, append it to the sequence, and continue the stop-at-one loop until the re...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the next_number tool with x=4, wait for the result, append it to the sequence, and continue the stop-at-one loop until t...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the next_number tool with x=4, wait for the result, append it to the sequence [8, 4], and continue the stop-at-one loop until the res...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the next_number tool with x=4, wait for the result, append it to the sequence [8, 4], and continue the stop-at-one loop until...[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_": "d50aa81c-7b61-4270-8b68-54504bc1d0a5",
"tool_name": "next_number",
"argu...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the next_number tool with x=2, wait for the result, append it to the sequence [8, 4, 2], and continue the stop-at-one loop until the ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the next_number tool with x=2, wait for the result, append it to the sequence [8, 4, 2], and continue the stop-at-one loop un...[TRUNCATED]
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, so the stop-at-one sequence is complete.
Complete sequence: 8 → 4 → 2 → 1
- Starting number: 8
- Total steps tak...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The tool returned 1, so the stop-at-one sequence is complete.
Complete sequence: 8 → 4 → 2 → 1
- Starting number: 8
- Total steps ...[TRUNCATED]
The tool returned 1, so the stop-at-one sequence is complete.
Complete sequence: 8 → 4 → 2 → 1
- Starting number: 8
- Total steps taken: 3
- Maximum value reached: 8
What to observe¶
Model-driven trace — the model needs an extra sub-step to decide to use the skill:
⚙️ Processing Step: Compute the stop-at-one sequence for 8. ← SubStep 1
✅ Step Result: I need to call the stop-at-one skill with the starting number 8.
🧠 New Step: Call the stop-at-one skill with the starting number 8.
⚙️ Processing Step: Call the stop-at-one skill with the starting number 8. ← SubStep 2
🛠️ Executing Tool Call: from_scratch__use_skill
✅ Successful Tool Call: <skill_content name="stop-at-one"> ...
SubStep 1 produces a text-only response with no tool call — the model reasons about what to do next. Only in SubStep 2 does it actually invoke from_scratch__use_skill.
User-explicit trace — the model calls the skill immediately:
⚙️ Processing Step: [pre-framed instruction to activate stop-at-one] ← SubStep 1
🛠️ Executing Tool Call: from_scratch__use_skill
✅ Successful Tool Call: <skill_content name="stop-at-one"> ...
The intermediate decision sub-step is eliminated. Both approaches produce the same final answer — the difference is that user-explicit activation saves one sub-step (and one LLM call) before skill execution begins.