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 examples/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.
# 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.
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.llms import OllamaLLM
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 = OllamaLLM(model="qwen3:14b", think=False)
agent = LLMAgent(llm=llm, tools=[next_number_tool])
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 compute the stop-at-one sequence for the starting number 8. I will begin by initializing the sequence with 8 and then repeate...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the `next_number` tool with the current value of `x` (which is 8) to generate the next number in the sequence. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the `next_number` tool with the current value of `x` (which is 8) to generate the next number in the sequence. 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: The result of calling the `next_number` tool with `x = 8` is `4`. I will append this to the sequence and continue the process. The upda...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: The assistant has initiated the process of computing the stop-at-one sequence for 8 and has successfully called the `next_number` tool wit...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: The assistant has initiated the process of computing the stop-at-one sequence for 8 and has successfully called the `next_number` ...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the `next_number` tool with the current value of `x` (which is 4) to generate the next number in the sequence. Let's pro...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the `next_number` tool with the current value of `x` (which is 4) to generate the next number in the sequence. Let's proceed with thi...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the `next_number` tool with the current value of `x` (which is 4) to generate the next number in the sequence. Let's proceed ...[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: The result of calling the `next_number` tool with `x = 4` is `2`. I will append this to the sequence, making it `[8, 4, 2]`. Let's cont...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the `next_number` tool with the current value of `x` (which is 2) to generate the next number in the sequence. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the `next_number` tool with the current value of `x` (which is 2) to generate the next number in the sequence. 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 result of calling the `next_number` tool with `x = 2` is `1`. I will append this to the sequence, making it `[8, 4, 2, 1]`. Since t...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : No new step required. INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The result of calling the `next_number` tool with `x = 2` is `1`. I will append this to the sequence, making it `[8, 4, 2, 1]`. Sinc...[TRUNCATED] The result of calling the `next_number` tool with `x = 2` is `1`. I will append this to the sequence, making it `[8, 4, 2, 1]`. Since the value 1 has been reached, the process is complete. Let me report the results: - **Starting number**: 8 - **Total steps taken**: 3 (tool calls made) - **Maximum value reached**: 8 The complete sequence is: `8 → 4 → 2 → 1`
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) : ✅ Step Result: I need to call the stop-at-one skill with the starting number 8. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the stop-at-one skill with the starting number 8. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the stop-at-one skill with the starting number 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 call the `next_number` tool with the starting number 8 to begin computing the sequence. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the `next_number` tool with the starting number 8 to begin computing the sequence. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the `next_number` tool with the starting number 8 to begin computing the sequence. 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: The result from the `next_number` tool is 4. I will append this to the sequence and continue the process. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: The next number in the sequence is 4. Continue the process by calling the `next_number` tool with the current number 4. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: The next number in the sequence is 4. Continue the process by calling the `next_number` tool with the current number 4. INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the `next_number` tool with the current number 4 to continue computing the sequence. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: 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: The result from the `next_number` tool is 2. I will append this to the sequence and continue the process. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the `next_number` tool with the current number 2 to continue computing the sequence. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the `next_number` tool with the current number 2 to continue computing the sequence. 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 result from the `next_number` tool is 1. The sequence has now reached the termination condition. I will finalize the sequence and r...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : No new step required. INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The result from the `next_number` tool is 1. The sequence has now reached the termination condition. I will finalize the sequence an...[TRUNCATED] The result from the `next_number` tool is 1. The sequence has now reached the termination condition. I will finalize the sequence and report the results. The complete sequence is: 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.