Plan Tool with Root Context
BeginnerToolsChainOfThoughtTemplates
Some planning tasks depend on details spread across earlier turns. The root assistant should not have to rewrite that context every time it wants a plan.
In this tutorial, the root assistant delegates planning to a plan tool that receives both the current task and the full conversation history.
The Problem
Users rarely ask for a plan in a single clean message. The real constraints usually appear across the conversation:
- the target plan or migration;
- the number of users or systems involved;
- the downtime limit;
- the approvals that must happen before launch.
If the root assistant has to restate all of that every time it calls a planning tool, the system becomes repetitive and fragile. The better pattern is to let the tool see the root conversation directly.
The Plan
We will build a small two-agent setup.
The root assistant handles the conversation. When the user asks for a rollout plan, checklist, roadmap, or next steps, it calls the plan tool.
The plan tool receives:
- the delegated
taskfrom the root assistant; - the full root conversation history through
inject_messages=True.
That gives the specialist enough context to write a useful plan without asking the root to repeat earlier details.
Architecture
Conversation history
+ current request
│
▼
RootAssistant
│
└── plan(task=...)
│
▼
PlanTool
inject_messages = True
│
├── sees root history
├── reasons with ChainOfThought
└── returns clean plan text
Setup
Setup your chat completion model (check dependencies)
Authenticate by setting the OPENAI_API_KEY env variable.
Authenticate by setting the GROQ_API_KEY env variable.
Install Ollama and pull your model first:
Authenticate by setting the OPENROUTER_API_KEY env variable.
Authenticate by setting the SAMBANOVA_API_KEY env variable.
Self-hosted with an OpenAI-compatible API:
Step 1 - The Plan Tool
The plan tool is itself an nn.Agent. It has no explicit Signature, so the default public annotation is task: str. That is exactly what the root assistant will pass.
inject_messages=True is the important part: the tool receives the root conversation history automatically.
import msgflux as mf
import msgflux.nn as nn
from msgflux.generation.reasoning import ChainOfThought
mf.load_dotenv()
model = mf.Model.chat_completion("openai/gpt-4.1-mini")
@mf.tool_config(inject_messages=True)
class PlanTool(nn.Agent):
"""Create an action plan using the delegated task and the full root conversation."""
name = "plan"
model = model
system_message = """
You are a planning specialist.
"""
instructions = """
Build plans using both the delegated task and the full conversation history.
Use the history to extract constraints, goals, deadlines, stakeholders, and risks.
Do not ignore details mentioned earlier in the conversation.
"""
expected_output = """
Return a concise action plan with:
- a one-line objective
- the key constraints
- 3 to 5 ordered steps
- the immediate next action
"""
generation_schema = ChainOfThought
templates = {"response": "{{ final_answer }}"}
config = {"verbose": True}
planner = PlanTool()
print(planner.annotations)
If you inspect planner.annotations, you will see the default public tool schema:
That means the root assistant can call the tool naturally with plan(task=...) without defining a separate signature.
The internal flow is:
- the root assistant passes a
taskstring to the tool; inject_messages=Truealso gives the tool the full root conversation history;ChainOfThoughtmakes the model produce bothreasoningandfinal_answer;templates={"response": "{{ final_answer }}"}strips the reasoning from the tool response and returns only the final plan text to the root assistant.
Without that template, the root assistant would receive the raw reasoning payload instead of a clean string.
Step 2 - The Root Assistant
The root assistant owns the conversation and decides when to delegate planning.
class RootAssistant(nn.Agent):
model = model
system_message = """
You are the root assistant for AcmeCloud.
"""
instructions = """
Use the plan tool whenever the user asks for a plan, rollout, checklist, roadmap,
or next steps.
Pass a clear task to the tool. The tool already receives the full conversation
history, so you do not need to restate all prior details in the task.
"""
tools = [planner]
config = {"verbose": True}
assistant = RootAssistant()
Examples
The conversation history is built with ChatBlock, not manual role dictionaries. The root assistant receives that history, and the plan tool sees the same history when it is called.
Example
history = [
mf.ChatBlock.user("We are moving from the Pro plan to Team next month for 40 users."),
mf.ChatBlock.assist(
"Understood. You need SAML SSO, audit logs, and a controlled migration."
),
mf.ChatBlock.user(
"We have two environments, one hour of downtime, and security needs sign-off before launch."
),
]
response = assistant(
"Create a rollout plan for this migration.",
messages=history,
)
print(response)
Expected behavior:
- the root assistant calls
plan(task=...); - the tool reads both the delegated task and the full root history;
- the plan includes constraints from earlier turns, like
40 users,two environments,one hour of downtime, andsecurity sign-off.
Complete Script
Expand full script
# /// script
# dependencies = []
# ///
import msgflux as mf
import msgflux.nn as nn
from msgflux.generation.reasoning import ChainOfThought
mf.load_dotenv()
model = mf.Model.chat_completion("openai/gpt-4.1-mini")
@mf.tool_config(inject_messages=True)
class PlanTool(nn.Agent):
"""Create an action plan using the delegated task and the full root conversation."""
name = "plan"
model = model
system_message = """
You are a planning specialist.
"""
instructions = """
Build plans using both the delegated task and the full conversation history.
Use the history to extract constraints, goals, deadlines, stakeholders, and risks.
Do not ignore details mentioned earlier in the conversation.
"""
expected_output = """
Return a concise action plan with:
- a one-line objective
- the key constraints
- 3 to 5 ordered steps
- the immediate next action
"""
generation_schema = ChainOfThought
templates = {"response": "{{ final_answer }}"}
config = {"verbose": True}
planner = PlanTool()
class RootAssistant(nn.Agent):
model = model
system_message = """
You are the root assistant for AcmeCloud.
"""
instructions = """
Use the plan tool whenever the user asks for a plan, rollout, checklist, roadmap,
or next steps.
Pass a clear task to the tool. The tool already receives the full conversation
history, so you do not need to restate all prior details in the task.
"""
tools = [planner]
config = {"verbose": True}
assistant = RootAssistant()
history = [
mf.ChatBlock.user("We are moving from the Pro plan to Team next month for 40 users."),
mf.ChatBlock.assist(
"Understood. You need SAML SSO, audit logs, and a controlled migration."
),
mf.ChatBlock.user(
"We have two environments, one hour of downtime, and security needs sign-off before launch."
),
]
print("Plan tool annotations:", planner.annotations)
print()
response = assistant(
"Create a rollout plan for this migration.",
messages=history,
)
print(response)
Further Reading
- Tools —
inject_messages=Trueand agent-as-tool patterns - Task and Context — conversation history with
messages - Generation Schemas —
ChainOfThoughtreasoning - Chat Completion —
ChatBlockhelpers and message formats