Python: Align ModeProvider tool names and instructions (#6071)

* Align ModeProvider tool names and instructions

* Address PR comments
This commit is contained in:
westey
2026-05-26 15:37:34 +01:00
committed by GitHub
Unverified
parent b2e77067e9
commit bd4fc64b4d
5 changed files with 83 additions and 52 deletions
@@ -14,14 +14,19 @@ from .._types import Message
DEFAULT_MODE_SOURCE_ID = "agent_mode"
DEFAULT_MODE_INSTRUCTIONS = (
"## Agent Mode\n\n"
"You can operate in different modes. Depending on the mode you are in, "
"you will be required to follow different processes.\n\n"
"Use the get_mode tool to check your current operating mode.\n"
"Use the set_mode tool to switch between modes as your work progresses. "
"Only use set_mode if the user explicitly instructs/allows you to change modes.\n\n"
"- You can operate in different modes. Depending on the mode you are in, "
"you will be required to follow different processes.\n"
"- You must check the current mode after any user input, since the user may have changed the mode themselves, "
"e.g. the user may have switched to 'plan' mode after a previous research task finished in 'execute' mode, "
"meaning they want to review a plan first before execution.\n\n"
"Use the mode_get tool to check your current operating mode.\n"
"Use the mode_set tool to switch between modes as your work progresses. "
"Only use mode_set if the user explicitly instructs/allows you to change modes.\n\n"
"You are currently operating in the {current_mode} mode.\n\n"
"### Mandatory Mode based Workflow\n\n"
"For every new substantive user request, including short factual questions, "
"your behavior is determined by the mode you are in.\n\n"
"{available_modes}\n"
"\n"
"You are currently operating in the {current_mode} mode.\n"
)
DEFAULT_MODE_CHANGE_NOTIFICATION = (
'[Mode changed: The operating mode has been switched from "{previous_mode}" to "{current_mode}". '
@@ -31,13 +36,37 @@ DEFAULT_MODE_DESCRIPTIONS: dict[str, str] = {
"plan": (
"Use this mode when analyzing requirements, breaking down tasks, and creating plans. "
"This is the interactive mode — ask clarifying questions, discuss options, and get user approval before "
"proceeding."
"proceeding.\n\n"
"Process to follow when in plan mode:\n"
"1. Analyze the request with the purpose of building a research plan.\n"
"2. Create a list of todo items.\n"
"3. If needed, use the provided tools to do some exploratory checks to help build a plan and determine "
"what clarifying questions you may need from the user.\n"
"4. Ask for clarifications from the user where needed.\n"
" 1. Ask each clarification one by one.\n"
" 2. When asking for clarification and you have specific options in mind, present them to the user, "
"so they can choose the option instead of having to retype the entire response.\n"
" 3. Do not proceed until you have received all the needed clarifications.\n"
" 4. Do short exploratory research if it helps with being able to ask sensible clarifications from "
"the user.\n"
"5. Write the plan to a memory file, so that it is retained even if compaction happens. "
"Make sure to update the plan file if the user requests changes.\n"
"6. Present the plan to the user and ask for approval to switch to execute mode and process the plan.\n"
"7. When approval is granted, always switch to execute mode (using the `mode_set` tool), "
"and follow the steps for *Execute mode*."
),
"execute": (
"Use this mode when carrying out approved plans. Work autonomously using your best judgement — do not ask "
"the user questions or wait for feedback. Make reasonable decisions on your own so that there is a complete, "
"useful result when the user returns. If you encounter ambiguity, choose the most reasonable option and note "
"your choice."
"Use this mode when carrying out approved plans. Work autonomously using your best judgment — do not ask "
"the user questions or wait for feedback.\n\n"
"Process to follow when in execute mode:\n"
"1. If you don't have a plan or tasks yet, analyze the user request and create tasks and a plan. "
"(**Skip this step if you came from plan mode**)\n"
"2. Work autonomously — use your best judgment to make decisions and keep progressing without asking "
"the user questions. The goal is to have a complete, useful result ready when the user returns.\n"
"3. If you encounter ambiguity or an unexpected situation during execution, choose the most reasonable "
"option, note your choice, and keep going.\n"
"4. Mark tasks as completed as you finish them.\n"
"5. Continue working, thinking and calling tools until you have the research result for the user."
),
}
@@ -179,8 +208,8 @@ class AgentModeProvider(ContextProvider):
``"plan"`` (interactive planning) and ``"execute"`` (autonomous execution).
This provider exposes the following tools to the agent:
- ``set_mode``: Switch the agent's operating mode.
- ``get_mode``: Retrieve the agent's current operating mode.
- ``mode_set``: Switch the agent's operating mode.
- ``mode_get``: Retrieve the agent's current operating mode.
Public helper functions ``get_agent_mode`` and ``set_agent_mode`` allow external code to programmatically read
and change the mode.
@@ -223,7 +252,7 @@ class AgentModeProvider(ContextProvider):
def _build_instructions(self, current_mode: str) -> str:
"""Build the mode guidance injected for the current session."""
mode_lines = "".join(
f'- "{self._mode_display_names[mode]}": {description}\n'
f"#### {self._mode_display_names[mode]}\n\n{description}\n\n"
for mode, description in self.mode_descriptions.items()
)
instructions = self.instructions or DEFAULT_MODE_INSTRUCTIONS
@@ -257,8 +286,8 @@ class AgentModeProvider(ContextProvider):
provider_state = _get_mode_state(session, source_id=self.source_id)
previous_mode = provider_state.pop(_PREVIOUS_MODE_STATE_KEY, None)
@tool(name="set_mode", approval_mode="never_require")
def set_mode(mode: str) -> str:
@tool(name="mode_set", approval_mode="never_require")
def mode_set(mode: str) -> str:
"""Switch the agent's operating mode."""
# The agent invoked the tool itself, so it knows the mode just changed — bypass
# ``set_agent_mode`` to avoid triggering a notification message on the next turn.
@@ -267,8 +296,8 @@ class AgentModeProvider(ContextProvider):
tool_state["current_mode"] = normalized_mode
return json.dumps({"mode": normalized_mode, "message": f"Mode changed to '{normalized_mode}'."})
@tool(name="get_mode", approval_mode="never_require")
def get_mode() -> str:
@tool(name="mode_get", approval_mode="never_require")
def mode_get() -> str:
"""Get the agent's current operating mode."""
current_mode_value = get_agent_mode(
session,
@@ -282,11 +311,11 @@ class AgentModeProvider(ContextProvider):
self.source_id,
[self._build_instructions(current_mode)],
)
context.extend_tools(self.source_id, [set_mode, get_mode])
context.extend_tools(self.source_id, [mode_set, mode_get])
if isinstance(previous_mode, str) and previous_mode != current_mode:
# Inject a user-role message announcing the external mode change. System instructions
# always render first in the chat history, so the agent can otherwise stay anchored to
# the most recent ``set_mode`` tool call rather than the new mode.
# the most recent ``mode_set`` tool call rather than the new mode.
previous_display = self._mode_display_names.get(previous_mode, previous_mode)
current_display = self._mode_display_names.get(current_mode, current_mode)
notification = DEFAULT_MODE_CHANGE_NOTIFICATION.format(
@@ -95,8 +95,10 @@ async def test_agent_mode_context_provider_normalizes_custom_modes(
)
instructions = options["instructions"]
assert isinstance(instructions, str)
assert '"Draft": Draft it.' in instructions
assert '"Final": Finalize it.' in instructions
assert "#### Draft" in instructions
assert "Draft it." in instructions
assert "#### Final" in instructions
assert "Finalize it." in instructions
assert "You are currently operating in the draft mode." in instructions
assert (
@@ -125,8 +127,8 @@ async def test_agent_mode_context_provider_serializes_tool_outputs_as_json(
)
tools = options["tools"]
assert isinstance(tools, list)
get_mode_tool = _tool_by_name(tools, "get_mode")
set_mode_tool = _tool_by_name(tools, "set_mode")
get_mode_tool = _tool_by_name(tools, "mode_get")
set_mode_tool = _tool_by_name(tools, "mode_set")
initial_mode = await get_mode_tool.invoke()
assert json.loads(initial_mode[0].text) == {"mode": mode_name}
@@ -152,13 +154,13 @@ async def test_agent_mode_context_provider_updates_agent_mode(
instructions = options["instructions"]
assert isinstance(instructions, str)
assert "## Agent Mode" in instructions
assert "Use the set_mode tool to switch between modes as your work progresses." in instructions
assert "Use the mode_set tool to switch between modes as your work progresses." in instructions
assert "ask clarifying questions, discuss options, and get user approval before proceeding" in instructions
assert "If you encounter ambiguity, choose the most reasonable option and note your choice" in instructions
assert "If you encounter ambiguity" in instructions
assert "You are currently operating in the plan mode." in instructions
get_mode_tool = _tool_by_name(tools, "get_mode")
set_mode_tool = _tool_by_name(tools, "set_mode")
get_mode_tool = _tool_by_name(tools, "mode_get")
set_mode_tool = _tool_by_name(tools, "mode_set")
initial_mode = await get_mode_tool.invoke()
assert json.loads(initial_mode[0].text) == {"mode": "plan"}
@@ -218,13 +220,13 @@ async def test_agent_mode_provider_injects_user_message_after_external_change(
provider = AgentModeProvider()
agent = Agent(client=chat_client_base, context_providers=[provider])
# First run: agent uses set_mode tool to switch to execute. The tool path must NOT queue a
# First run: agent uses mode_set tool to switch to execute. The tool path must NOT queue a
# notification because the agent already saw its own tool call in the chat history.
_, first_options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage]
session=session,
input_messages=[Message(role="user", contents=["Plan first."])],
)
set_mode_tool = _tool_by_name(first_options["tools"], "set_mode")
set_mode_tool = _tool_by_name(first_options["tools"], "mode_set")
await set_mode_tool.invoke(arguments={"mode": "execute"})
assert "previous_mode_for_notification" not in session.state[provider.source_id]