Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
namespace Harness.Shared.Console.ToolFormatters;

/// <summary>
/// Formats <c>AgentMode_*</c> tool calls, showing the target mode for Set operations.
/// Formats <c>mode_*</c> tool calls, showing the target mode for Set operations.
/// </summary>
public sealed class ModeToolFormatter : ToolCallFormatter
{
/// <inheritdoc/>
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("AgentMode_", StringComparison.Ordinal);
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("mode_", StringComparison.Ordinal);

/// <inheritdoc/>
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
{
"AgentMode_Set" => FormatStringArg(call, "mode"),
"mode_set" => FormatStringArg(call, "mode"),
Comment thread
westey-m marked this conversation as resolved.
_ => null,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ namespace Microsoft.Agents.AI;
/// <para>
/// This provider exposes the following tools to the agent:
/// <list type="bullet">
/// <item><description><c>AgentMode_Set</c> — Switch the agent's operating mode.</description></item>
/// <item><description><c>AgentMode_Get</c> — Retrieve the agent's current operating mode.</description></item>
/// <item><description><c>mode_set</c> — Switch the agent's operating mode.</description></item>
/// <item><description><c>mode_get</c> — Retrieve the agent's current operating mode.</description></item>
/// </list>
/// </para>
/// <para>
Expand All @@ -49,8 +49,8 @@ public sealed class AgentModeProvider : AIContextProvider
- 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.

Use the AgentMode_Get tool to check your current operating mode.
Use the AgentMode_Set tool to switch between modes as your work progresses. Only use AgentMode_Set if the user explicitly instructs/allows you to change modes.
Use the mode_get tool to check your current operating mode.
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.

You are currently operating in the {current_mode} mode.

Expand Down Expand Up @@ -79,7 +79,7 @@ 3. Do not proceed until you have received all the needed clarifications.
4. Do short exploratory research if it helps with being able to ask sensible clarifications from the user.
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.
6. Present the plan to the user and ask for approval to switch to execute mode and process the plan.
7. When approval is granted, always switch to execute mode (using the `AgentMode_Set` tool), and follow the steps for *Execute mode*.
7. When approval is granted, always switch to execute mode (using the `mode_set` tool), and follow the steps for *Execute mode*.
"""),
new(
"execute",
Expand Down Expand Up @@ -263,7 +263,7 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session)
},
new AIFunctionFactoryOptions
{
Name = "AgentMode_Set",
Name = "mode_set",
Description = $"Switch the agent's operating mode. Supported modes: \"{this._modeNamesDisplay}\".",
SerializerOptions = serializerOptions,
}),
Expand All @@ -272,7 +272,7 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session)
() => state.CurrentMode,
new AIFunctionFactoryOptions
{
Name = "AgentMode_Get",
Name = "mode_get",
Description = "Get the agent's current operating mode.",
SerializerOptions = serializerOptions,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public async Task SetMode_ChangesModeAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
AIFunction setMode = GetTool(tools, "AgentMode_Set");
AIFunction setMode = GetTool(tools, "mode_set");

// Act
await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
Expand All @@ -90,7 +90,7 @@ public async Task SetMode_ReturnsConfirmationAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
AIFunction setMode = GetTool(tools, "AgentMode_Set");
AIFunction setMode = GetTool(tools, "mode_set");

// Act
object? result = await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
Expand All @@ -107,8 +107,8 @@ public async Task SetMode_InvalidMode_ThrowsAsync()
{
// Arrange
var (tools, provider, session) = await CreateToolsWithProviderAndSessionAsync();
AIFunction setMode = GetTool(tools, "AgentMode_Set");
AIFunction getMode = GetTool(tools, "AgentMode_Get");
AIFunction setMode = GetTool(tools, "mode_set");
AIFunction getMode = GetTool(tools, "mode_get");

// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(async () =>
Expand All @@ -131,7 +131,7 @@ public async Task GetMode_ReturnsDefaultModeAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
AIFunction getMode = GetTool(tools, "AgentMode_Get");
AIFunction getMode = GetTool(tools, "mode_get");

// Act
object? result = await getMode.InvokeAsync(new AIFunctionArguments());
Expand All @@ -148,8 +148,8 @@ public async Task GetMode_ReturnsUpdatedModeAfterSetAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
AIFunction setMode = GetTool(tools, "AgentMode_Set");
AIFunction getMode = GetTool(tools, "AgentMode_Get");
AIFunction setMode = GetTool(tools, "mode_set");
AIFunction getMode = GetTool(tools, "mode_get");

// Act
await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
Expand Down Expand Up @@ -236,7 +236,7 @@ public async Task PublicSetMode_ReflectedInToolResultsAsync()

// Act
AIContext result = await provider.InvokingAsync(context);
AIFunction getMode = GetTool(result.Tools!, "AgentMode_Get");
AIFunction getMode = GetTool(result.Tools!, "mode_get");
object? modeResult = await getMode.InvokeAsync(new AIFunctionArguments());

// Assert
Expand Down Expand Up @@ -264,12 +264,12 @@ public async Task State_PersistsAcrossInvocationsAsync()

// Act — first invocation changes mode
AIContext result1 = await provider.InvokingAsync(context);
AIFunction setMode = GetTool(result1.Tools!, "AgentMode_Set");
AIFunction setMode = GetTool(result1.Tools!, "mode_set");
await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });

// Second invocation should see the updated mode
AIContext result2 = await provider.InvokingAsync(context);
AIFunction getMode = GetTool(result2.Tools!, "AgentMode_Get");
AIFunction getMode = GetTool(result2.Tools!, "mode_get");
object? modeResult = await getMode.InvokeAsync(new AIFunctionArguments());

// Assert
Expand Down Expand Up @@ -579,7 +579,7 @@ public async Task ToolModeChange_DoesNotInjectNotificationAsync()

// First call to initialize
AIContext result1 = await provider.InvokingAsync(context);
AIFunction setMode = GetTool(result1.Tools!, "AgentMode_Set");
AIFunction setMode = GetTool(result1.Tools!, "mode_set");

// Change mode via the tool (agent-initiated)
await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
Expand Down
71 changes: 50 additions & 21 deletions python/packages/core/agent_framework/_harness/_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
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}". '
Expand All @@ -31,13 +36,37 @@
"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."
),
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -223,7 +252,7 @@ def __init__(
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
Expand Down Expand Up @@ -257,8 +286,8 @@ async def before_run(
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.
Expand All @@ -267,8 +296,8 @@ def set_mode(mode: str) -> str:
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,
Expand All @@ -282,11 +311,11 @@ def get_mode() -> str:
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(
Expand Down
22 changes: 12 additions & 10 deletions python/packages/core/tests/core/test_harness_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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}
Expand All @@ -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"}
Expand Down Expand Up @@ -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]

Expand Down
Loading