diff --git a/frontend/__tests__/components/event-message.test.tsx b/frontend/__tests__/components/event-message.test.tsx index f197d2d8ab..0cab1a032d 100644 --- a/frontend/__tests__/components/event-message.test.tsx +++ b/frontend/__tests__/components/event-message.test.tsx @@ -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", }, diff --git a/frontend/src/components/features/chat/event-content-helpers/get-action-content.ts b/frontend/src/components/features/chat/event-content-helpers/get-action-content.ts index 7501b03ee7..f803434fd9 100644 --- a/frontend/src/components/features/chat/event-content-helpers/get-action-content.ts +++ b/frontend/src/components/features/chat/event-content-helpers/get-action-content.ts @@ -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 => { diff --git a/frontend/src/types/core/actions.ts b/frontend/src/types/core/actions.ts index 50ae0cc6e0..9c5dd81bc4 100644 --- a/frontend/src/types/core/actions.ts +++ b/frontend/src/types/core/actions.ts @@ -64,7 +64,6 @@ export interface FinishAction extends OpenHandsActionEvent<"finish"> { source: "agent"; args: { final_thought: string; - task_completed: "success" | "failure" | "partial"; outputs: Record; thought: string; }; diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 6a8354f872..44dc93b755 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -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), ) # ================================================ diff --git a/openhands/agenthub/codeact_agent/tools/finish.py b/openhands/agenthub/codeact_agent/tools/finish.py index bc0018e3e7..033d0f3df0 100644 --- a/openhands/agenthub/codeact_agent/tools/finish.py +++ b/openhands/agenthub/codeact_agent/tools/finish.py @@ -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.', - }, }, }, ), diff --git a/openhands/agenthub/loc_agent/function_calling.py b/openhands/agenthub/loc_agent/function_calling.py index 32fac63cb6..06cb01fe49 100644 --- a/openhands/agenthub/loc_agent/function_calling.py +++ b/openhands/agenthub/loc_agent/function_calling.py @@ -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( diff --git a/openhands/agenthub/readonly_agent/function_calling.py b/openhands/agenthub/readonly_agent/function_calling.py index 19ad0ad5f8..fded8d324b 100644 --- a/openhands/agenthub/readonly_agent/function_calling.py +++ b/openhands/agenthub/readonly_agent/function_calling.py @@ -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), ) # ================================================ diff --git a/openhands/events/action/agent.py b/openhands/events/action/agent.py index daedb9c42a..c81d6033f8 100644 --- a/openhands/events/action/agent.py +++ b/openhands/events/action/agent.py @@ -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 diff --git a/openhands/events/serialization/action.py b/openhands/events/serialization/action.py index 86751999c4..18562b076f 100644 --- a/openhands/events/serialization/action.py +++ b/openhands/events/serialization/action.py @@ -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') diff --git a/openhands/events/serialization/event.py b/openhands/events/serialization/event.py index 1bd669c7c3..f12f0580e3 100644 --- a/openhands/events/serialization/event.py +++ b/openhands/events/serialization/event.py @@ -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: diff --git a/openhands/llm/fn_call_converter.py b/openhands/llm/fn_call_converter.py index 73eba911c6..a79ec4f2c2 100644 --- a/openhands/llm/fn_call_converter.py +++ b/openhands/llm/fn_call_converter.py @@ -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! 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. -true """ }, @@ -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 --------------------- diff --git a/tests/unit/test_action_serialization.py b/tests/unit/test_action_serialization.py index a7aaa263fa..9c31485673 100644 --- a/tests/unit/test_action_serialization.py +++ b/tests/unit/test_action_serialization.py @@ -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', diff --git a/tests/unit/test_llm_fncall_converter.py b/tests/unit/test_llm_fncall_converter.py index 52274f8abe..d29cd0b958 100644 --- a/tests/unit/test_llm_fncall_converter.py +++ b/tests/unit/test_llm_fncall_converter.py @@ -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 diff --git a/tests/unit/test_security.py b/tests/unit/test_security.py index 44f9d6adc5..e9fa6c68bc 100644 --- a/tests/unit/test_security.py +++ b/tests/unit/test_security.py @@ -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': '', }, ),