Remove task completion status message from finish action display (#9977)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
Xingyao Wang 2025-07-30 16:33:45 -04:00 committed by GitHub
parent 6f44b7352e
commit c2fc84e6ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 41 additions and 52 deletions

View File

@ -28,7 +28,6 @@ describe("EventMessage", () => {
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},
@ -114,7 +113,6 @@ describe("EventMessage", () => {
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},

View File

@ -77,25 +77,8 @@ const getMcpActionContent = (event: MCPAction): string => {
const getThinkActionContent = (event: ThinkAction): string =>
event.args.thought;
const getFinishActionContent = (event: FinishAction): string => {
let content = event.args.final_thought;
switch (event.args.task_completed) {
case "success":
content += `\n\n\n${i18n.t("FINISH$TASK_COMPLETED_SUCCESSFULLY")}`;
break;
case "failure":
content += `\n\n\n${i18n.t("FINISH$TASK_NOT_COMPLETED")}`;
break;
case "partial":
default:
content += `\n\n\n${i18n.t("FINISH$TASK_COMPLETED_PARTIALLY")}`;
break;
}
return content.trim();
};
const getFinishActionContent = (event: FinishAction): string =>
event.args.final_thought.trim();
const getNoContentActionContent = (): string => "";
export const getActionContent = (event: OpenHandsAction): string => {

View File

@ -64,7 +64,6 @@ export interface FinishAction extends OpenHandsActionEvent<"finish"> {
source: "agent";
args: {
final_thought: string;
task_completed: "success" | "failure" | "partial";
outputs: Record<string, unknown>;
thought: string;
};

View File

@ -123,7 +123,6 @@ def response_to_actions(
elif tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
task_completed=arguments.get('task_completed', None),
)
# ================================================

View File

@ -13,8 +13,6 @@ The message should include:
- Any next steps for the user
- Explanation if you're unable to complete the task
- Any follow-up questions if more information is needed
The task_completed field should be set to True if you believed you have completed the task, and False otherwise.
"""
FinishTool = ChatCompletionToolParam(
@ -24,17 +22,12 @@ FinishTool = ChatCompletionToolParam(
description=_FINISH_DESCRIPTION,
parameters={
'type': 'object',
'required': ['message', 'task_completed'],
'required': ['message'],
'properties': {
'message': {
'type': 'string',
'description': 'Final message to send to the user',
},
'task_completed': {
'type': 'string',
'enum': ['true', 'false', 'partial'],
'description': 'Whether you have completed the task.',
},
},
},
),

View File

@ -81,7 +81,6 @@ def response_to_actions(
elif tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
task_completed=arguments.get('task_completed', None),
)
else:
raise FunctionCallNotExistsError(

View File

@ -141,7 +141,6 @@ def response_to_actions(
if tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
task_completed=arguments.get('task_completed', None),
)
# ================================================

View File

@ -1,5 +1,4 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from openhands.core.schema import ActionType
@ -20,26 +19,18 @@ class ChangeAgentStateAction(Action):
return f'Agent state changed to {self.agent_state}'
class AgentFinishTaskCompleted(Enum):
FALSE = 'false'
PARTIAL = 'partial'
TRUE = 'true'
@dataclass
class AgentFinishAction(Action):
"""An action where the agent finishes the task.
Attributes:
final_thought (str): The message to send to the user.
task_completed (enum): Whether the agent believes the task has been completed.
outputs (dict): The other outputs of the agent, for instance "content".
thought (str): The agent's explanation of its actions.
action (str): The action type, namely ActionType.FINISH.
"""
final_thought: str = ''
task_completed: AgentFinishTaskCompleted | None = None
outputs: dict[str, Any] = field(default_factory=dict)
thought: str = ''
action: str = ActionType.FINISH

View File

@ -56,6 +56,10 @@ def handle_action_deprecated_args(args: dict[str, Any]) -> dict[str, Any]:
if 'keep_prompt' in args:
args.pop('keep_prompt')
# task_completed has been deprecated - remove it from args to maintain backward compatibility
if 'task_completed' in args:
args.pop('task_completed')
# Handle translated_ipython_code deprecation
if 'translated_ipython_code' in args:
code = args.pop('translated_ipython_code')

View File

@ -121,6 +121,9 @@ def event_to_dict(event: 'Event') -> dict:
props.pop(key, None)
if 'security_risk' in props and props['security_risk'] is None:
props.pop('security_risk')
# Remove task_completed from serialization when it's None (backward compatibility)
if 'task_completed' in props and props['task_completed'] is None:
props.pop('task_completed')
if 'action' in d:
d['args'] = props
if event.timeout is not None:

View File

@ -306,12 +306,11 @@ Review the changes and make sure they are as expected. Edit the file again if ne
""",
},
'finish': {
'task_completed': """
'example': """
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<function=finish>
<parameter=message>The task has been completed. The web server is running and displaying numbers 1-10 in a table format at http://127.0.0.1:5000.</parameter>
<parameter=task_completed>true</parameter>
</function>
"""
},
@ -373,7 +372,7 @@ USER: Create a list of numbers from 1 to 10, and display them in a web page at p
example += TOOL_EXAMPLES['execute_bash']['run_server_again']
if 'finish' in available_tools:
example += TOOL_EXAMPLES['finish']['task_completed']
example += TOOL_EXAMPLES['finish']['example']
example += """
--------------------- END OF EXAMPLE ---------------------

View File

@ -75,13 +75,36 @@ def test_agent_finish_action_serialization_deserialization():
'args': {
'outputs': {},
'thought': '',
'task_completed': None,
'final_thought': '',
},
}
serialization_deserialization(original_action_dict, AgentFinishAction)
def test_agent_finish_action_legacy_task_completed_serialization():
"""Test that old conversations with task_completed can still be loaded."""
original_action_dict = {
'action': 'finish',
'args': {
'outputs': {},
'thought': '',
'final_thought': 'Task completed',
'task_completed': 'true', # This should be ignored during deserialization
},
}
# This should work without errors - task_completed should be stripped out
event = event_from_dict(original_action_dict)
assert isinstance(event, Action)
assert isinstance(event, AgentFinishAction)
assert event.final_thought == 'Task completed'
# task_completed attribute should not exist anymore
assert not hasattr(event, 'task_completed')
# When serialized back, task_completed should not be present
event_dict = event_to_dict(event)
assert 'task_completed' not in event_dict['args']
def test_agent_reject_action_serialization_deserialization():
original_action_dict = {
'action': 'reject',

View File

@ -183,7 +183,7 @@ def test_get_example_for_tools_single_tool():
assert TOOL_EXAMPLES['execute_bash']['kill_server'] in example
assert TOOL_EXAMPLES['str_replace_editor']['create_file'] not in example
assert TOOL_EXAMPLES['browser']['view_page'] not in example
assert TOOL_EXAMPLES['finish']['task_completed'] not in example
assert TOOL_EXAMPLES['finish']['example'] not in example
def test_get_example_for_tools_single_tool_is_finish():
@ -205,7 +205,7 @@ def test_get_example_for_tools_single_tool_is_finish():
'USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.'
in example
)
assert TOOL_EXAMPLES['finish']['task_completed'] in example
assert TOOL_EXAMPLES['finish']['example'] in example
assert TOOL_EXAMPLES['execute_bash']['check_dir'] not in example
assert TOOL_EXAMPLES['str_replace_editor']['create_file'] not in example
assert TOOL_EXAMPLES['browser']['view_page'] not in example
@ -274,7 +274,7 @@ def test_get_example_for_tools_multiple_tools():
assert TOOL_EXAMPLES['str_replace_editor']['create_file'] in example
assert TOOL_EXAMPLES['str_replace_editor']['edit_file'] in example
assert TOOL_EXAMPLES['browser']['view_page'] not in example
assert TOOL_EXAMPLES['finish']['task_completed'] not in example
assert TOOL_EXAMPLES['finish']['example'] not in example
def test_get_example_for_tools_multiple_tools_with_finish():
@ -374,7 +374,7 @@ def test_get_example_for_tools_multiple_tools_with_finish():
assert TOOL_EXAMPLES['browser']['view_page'] in example
# Check for finish part
assert TOOL_EXAMPLES['finish']['task_completed'] in example
assert TOOL_EXAMPLES['finish']['example'] in example
def test_get_example_for_tools_all_tools():
@ -393,7 +393,7 @@ def test_get_example_for_tools_all_tools():
assert TOOL_EXAMPLES['execute_bash']['kill_server'] in example
assert TOOL_EXAMPLES['str_replace_editor']['create_file'] in example
assert TOOL_EXAMPLES['str_replace_editor']['edit_file'] in example
assert TOOL_EXAMPLES['finish']['task_completed'] in example
assert TOOL_EXAMPLES['finish']['example'] in example
# These are not in global FNCALL_TOOLS
# assert TOOL_EXAMPLES['web_read']['read_docs'] not in example # web_read is removed

View File

@ -349,7 +349,6 @@ async def test_unsafe_bash_command(temp_dir: str):
name=ActionType.FINISH,
arguments={
'outputs': {'content': 'outputs content'},
'task_completed': None,
'final_thought': '',
},
),