Repository: All-Hands-AI/OpenHands
Pull request: Add back microagent files with special handling for user inputs
- [x] This change is worth documenting at https://docs.all-hands.dev/
- [x] Include this change in the Release Notes. If checked, you **must** provide an **end-user friendly** description for your change below

**End-user friendly description of the problem this fixes or functionality this introduces.**

This PR adds microagent templates with user input support, allowing users to trigger specialized workflows using command-style triggers (e.g., "/fix_test"). When typing "/" in the chat input, a dropdown of available microagents appears, making it easier to discover and use these specialized workflows. The microagents will prompt for any required inputs, streamlining common development tasks.

---
**Summarize what the PR does, explaining any non-trivial design decisions.**

This PR adds back the microagent files that were removed in PR #8122 and adds special handling for these microagents that expect user inputs.

Changes:

1. Added back 5 microagent files with triggers:
   - add_repo_instruction.md with trigger `/add_repo_instruction`
   - address_pr_comments.md with trigger `/address_pr_comments`
   - get_test_to_pass.md with trigger `/fix_test`
   - update_pr_description.md with trigger `/update_pr`
   - update_test_for_new_implementation.md with trigger `/update_test`

2. Added special handling in `openhands/microagent/microagent.py`:
   - Added variable extraction functionality to detect ${variable} patterns
   - Modified trigger matching to handle command-style triggers with "/"
   - Added user input detection for microagents with variables
   - Added prompt appending for task microagents

3. Added unit tests to verify the functionality

4. Implemented microagent suggestions using TipTap:
   - Replaced custom implementation with TipTap editor
   - Added mention extension for microagent suggestions
   - Improved styling and user experience
   - Added tests for the new implementation
   - Fixed linting issues in TipTap editor component

These microagents can now be triggered with a command-style format (e.g., "/fix_test") and will prompt the user for any required inputs. When typing "/" in the chat input, a dropdown of available microagents will appear.

![image](https://github.com/user-attachments/assets/563a1397-6cd4-458b-8995-e5c737022b6e)
![image](https://github.com/user-attachments/assets/eb37356b-0146-4363-a899-574218b5b32a)

---
**Link of any specific issues this addresses:**

Fix https://github.com/All-Hands-AI/OpenHands/issues/5826

---

To run this PR locally, use the following command:
```
docker run -it --rm   -p 3000:3000   -v /var/run/docker.sock:/var/run/docker.sock   --add-host host.docker.internal:host-gateway   -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:58845b0-nikolaik   --name openhands-app-58845b0   docker.all-hands.dev/all-hands-ai/openhands:58845b0
```

Problem statement:
[Feature]: Add input-based micro-agents to support template workflows
**What problem or use case are you trying to solve?**
We need to extend micro-agents to support template-based workflows. Currently, micro-agents are triggered by keywords and inject specialized knowledge (e.g., GitHub API understanding). However, users often find themselves repeatedly typing similar prompts that could be templatized.

For example, a template for addressing PR comments could be a micro-agent that accepts inputs:
```
Can you look at this PR ${PR_URL}
And then check out to branch ${BRANCH_NAME} which corresponds to that PR
Then, read the diff against the main, understand the purpose of this PR, and then address the PR comments.
${USER_MESSAGE}
```

**Describe the UX of the solution you'd like**
Micro-agents would have two triggering mechanisms:
1. Traditional keyword-based triggers (e.g., `.openhands/microagents/*.md`)
2. UI-based selection where users can pick a micro-agent and provide required inputs via a form in FE

For future: If the user did NOT provide those inputs, the micro-agent itself can conversationally ask users for the required information.

**Do you have thoughts on the technical implementation?**
- Add an `inputs` parameter to the micro-agent system to specify required template variables
- Store micro-agent templates in the repository (e.g., in the existing micro-agents location)
- Add UI elements:
  - Button to create new micro-agent templates
  - "Save" button to automatically create PRs against the repository
- Future potential: Build an API to trigger these micro-agents (e.g., `/start_session?workflow=resolve_issue`)

**Additional context**
This enhancement maintains the existing micro-agent concept while adding template capabilities. Rather than creating separate systems for specialized knowledge injection and templated workflows, we're unifying them under the micro-agent framework with support for both keyword triggers and user-supplied inputs.

How's this revised version that maintains the micro-agent terminology while incorporating the template functionality?

Hints from issue discussion:
This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.
This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.
This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.
@OpenHands can you look into this, and come up with a plan of how we can implement this? You should look under the "microagents/" folder, as well as the current implementation of micro-agents in the backend.

Then look at frontend/ folder and try to come up maybe 3-5 different approaches of how we can support these "task" microagents in frontend UI, and how would they interact with backend server.
Uh oh! There was an unexpected error starting the job :(

Commit message: 🤖 Auto-fix Python linting issues

Patch to review:
diff --git a/microagents/add_openhands_repo_instruction.md b/microagents/add_openhands_repo_instruction.md
new file mode 100644
index 000000000000..c016af84e96a
--- /dev/null
+++ b/microagents/add_openhands_repo_instruction.md
@@ -0,0 +1,67 @@
+---
+name: add_repo_instruction
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /add_repo_instruction
+inputs:
+  - name: REPO_FOLDER_NAME
+    description: "Branch for the agent to work on"
+---
+
+Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
+
+Specifically, I want you to create a `.openhands/microagents/repo.md`  file. This file should contain succinct information that summarizes (1) the purpose of this repository, (2) the general setup of this repo, and (3) a brief description of the structure of this repo.
+
+Here's an example:
+```markdown
+---
+name: repo
+type: repo
+agent: CodeActAgent
+---
+
+This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
+(in the `openhands` directory) and React frontend (in the `frontend` directory).
+
+## General Setup:
+To set up the entire repo, including frontend and backend, run `make build`.
+You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
+
+Before pushing any changes, you should ensure that any lint errors or simple test errors have been fixed.
+
+* If you've made changes to the backend, you should run `pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml`
+* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
+
+If either command fails, it may have automatically fixed some issues. You should fix any issues that weren't automatically fixed,
+then re-run the command to ensure it passes.
+
+## Repository Structure
+Backend:
+- Located in the `openhands` directory
+- Testing:
+  - All tests are in `tests/unit/test_*.py`
+  - To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
+  - Write all tests with pytest
+
+Frontend:
+- Located in the `frontend` directory
+- Prerequisites: A recent version of NodeJS / NPM
+- Setup: Run `npm install` in the frontend directory
+- Testing:
+  - Run tests: `npm run test`
+  - To run specific tests: `npm run test -- -t "TestName"`
+- Building:
+  - Build for production: `npm run build`
+- Environment Variables:
+  - Set in `frontend/.env` or as environment variables
+  - Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
+- Internationalization:
+  - Generate i18n declaration file: `npm run make-i18n`
+```
+
+Now, please write a similar markdown for the current repository.
+Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.
+
+If the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task.
diff --git a/microagents/address_pr_comments.md b/microagents/address_pr_comments.md
new file mode 100644
index 000000000000..9691802e12d3
--- /dev/null
+++ b/microagents/address_pr_comments.md
@@ -0,0 +1,21 @@
+---
+name: address_pr_comments
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /address_pr_comments
+inputs:
+  - name: PR_URL
+    description: "URL of the pull request"
+  - name: BRANCH_NAME
+    description: "Branch name corresponds to the pull request"
+---
+
+First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
+
+This branch corresponds to this PR {{ PR_URL }}
+
+Next, you should use the GitHub API to read the reviews and comments on this PR and address them.
+
+If the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task.
diff --git a/microagents/get_test_to_pass.md b/microagents/get_test_to_pass.md
new file mode 100644
index 000000000000..71171144a193
--- /dev/null
+++ b/microagents/get_test_to_pass.md
@@ -0,0 +1,27 @@
+---
+name: get_test_to_pass
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /fix_test
+inputs:
+  - name: BRANCH_NAME
+    description: "Branch for the agent to work on"
+  - name: TEST_COMMAND_TO_RUN
+    description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
+  - name: FUNCTION_TO_FIX
+    description: "The name of function to fix"
+  - name: FILE_FOR_FUNCTION
+    description: "The path of the file that contains the function"
+---
+
+Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
+
+{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
+Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
+{%- endif %}
+
+PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.
+
+If the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task.
diff --git a/microagents/update_pr_description.md b/microagents/update_pr_description.md
new file mode 100644
index 000000000000..ff22603f169d
--- /dev/null
+++ b/microagents/update_pr_description.md
@@ -0,0 +1,23 @@
+---
+name: update_pr_description
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /update_pr
+inputs:
+  - name: PR_URL
+    description: "URL of the pull request"
+    type: string
+    validation:
+      pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
+  - name: BRANCH_NAME
+    description: "Branch name corresponds to the pull request"
+    type: string
+---
+
+Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
+
+Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.
+
+If the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task.
diff --git a/microagents/update_test_for_new_implementation.md b/microagents/update_test_for_new_implementation.md
new file mode 100644
index 000000000000..7ce35492ca7e
--- /dev/null
+++ b/microagents/update_test_for_new_implementation.md
@@ -0,0 +1,23 @@
+---
+name: update_test_for_new_implementation
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /update_test
+inputs:
+  - name: BRANCH_NAME
+    description: "Branch for the agent to work on"
+  - name: TEST_COMMAND_TO_RUN
+    description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
+---
+
+Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
+
+{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
+Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
+{%- endif %}
+
+PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.
+
+If the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task.
diff --git a/openhands/memory/memory.py b/openhands/memory/memory.py
index 0042af283194..c64c71b78b07 100644
--- a/openhands/memory/memory.py
+++ b/openhands/memory/memory.py
@@ -219,11 +219,18 @@ def _find_microagent_knowledge(self, query: str) -> list[MicroagentKnowledge]:
             trigger = microagent.match_trigger(query)
             if trigger:
                 logger.info("Microagent '%s' triggered by keyword '%s'", name, trigger)
+
+                # Check if this is a special microagent that requires user input
+                content = microagent.content
+                if microagent.requires_user_input():
+                    # Add a prompt at the end of the content to ask for missing variables
+                    content += "\n\nIf the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task."
+
                 recalled_content.append(
                     MicroagentKnowledge(
                         name=microagent.name,
                         trigger=trigger,
-                        content=microagent.content,
+                        content=content,
                     )
                 )
         return recalled_content
diff --git a/openhands/microagent/microagent.py b/openhands/microagent/microagent.py
index c1b18ca79468..006faaaa650e 100644
--- a/openhands/microagent/microagent.py
+++ b/openhands/microagent/microagent.py
@@ -1,6 +1,7 @@
 import io
+import re
 from pathlib import Path
-from typing import Union
+from typing import List, Union
 
 import frontmatter
 from pydantic import BaseModel
@@ -9,7 +10,7 @@
     MicroagentValidationError,
 )
 from openhands.core.logger import openhands_logger as logger
-from openhands.microagent.types import MicroagentMetadata, MicroagentType
+from openhands.microagent.types import InputMetadata, MicroagentMetadata, MicroagentType
 
 
 class BaseMicroagent(BaseModel):
@@ -71,13 +72,24 @@ def load(
         subclass_map = {
             MicroagentType.KNOWLEDGE: KnowledgeMicroagent,
             MicroagentType.REPO_KNOWLEDGE: RepoMicroagent,
+            MicroagentType.TASK: TaskMicroagent,
         }
 
         # Infer the agent type:
-        # 1. If triggers exist -> KNOWLEDGE
-        # 2. Else (no triggers) -> REPO
+        # 1. If inputs exist -> TASK
+        # 2. If triggers exist -> KNOWLEDGE
+        # 3. Else (no triggers) -> REPO
         inferred_type: MicroagentType
-        if metadata.triggers:
+        if metadata.inputs:
+            inferred_type = MicroagentType.TASK
+            # Add a trigger for the agent name if not already present
+            trigger = f'/{metadata.name}'
+            if not metadata.triggers or trigger not in metadata.triggers:
+                if not metadata.triggers:
+                    metadata.triggers = [trigger]
+                else:
+                    metadata.triggers.append(trigger)
+        elif metadata.triggers:
             inferred_type = MicroagentType.KNOWLEDGE
         else:
             # No triggers, default to REPO unless metadata explicitly says otherwise (which it shouldn't for REPO)
@@ -121,7 +133,11 @@ def match_trigger(self, message: str) -> str | None:
         """
         message = message.lower()
         for trigger in self.triggers:
-            if trigger.lower() in message:
+            # Check for exact match for triggers starting with "/"
+            if trigger.startswith('/') and trigger.lower() == message.strip().lower():
+                return trigger
+            # Otherwise, check for substring match
+            elif not trigger.startswith('/') and trigger.lower() in message:
                 return trigger
         return None
 
@@ -129,6 +145,24 @@ def match_trigger(self, message: str) -> str | None:
     def triggers(self) -> list[str]:
         return self.metadata.triggers
 
+    def extract_variables(self, content: str) -> List[str]:
+        """Extract variables from the content.
+
+        Variables are in the format ${variable_name}.
+        """
+        pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
+        matches = re.findall(pattern, content)
+        return matches
+
+    def requires_user_input(self) -> bool:
+        """Check if this microagent requires user input.
+
+        Returns True if the content contains variables in the format ${variable_name}.
+        """
+        # Check if the content contains any variables
+        variables = self.extract_variables(self.content)
+        return len(variables) > 0
+
 
 class RepoMicroagent(BaseMicroagent):
     """Microagent specialized for repository-specific knowledge and guidelines.
@@ -150,6 +184,75 @@ def __init__(self, **data):
             )
 
 
+class TaskMicroagent(BaseMicroagent):
+    """Microagent specialized for tasks that require user input.
+
+    These microagents are triggered by a special format: "/{agent_name}"
+    and will prompt the user for any required inputs before proceeding.
+    """
+
+    def __init__(self, **data):
+        super().__init__(**data)
+        if self.type != MicroagentType.TASK:
+            raise ValueError(
+                f'TaskMicroagent initialized with incorrect type: {self.type}'
+            )
+
+        # Append a prompt to ask for missing variables
+        self._append_missing_variables_prompt()
+
+    def _append_missing_variables_prompt(self) -> None:
+        """Append a prompt to ask for missing variables."""
+        # Check if the content contains any variables or has inputs defined
+        if not self.requires_user_input() and not self.metadata.inputs:
+            return
+
+        prompt = "\n\nIf the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task."
+        self.content += prompt
+
+    def match_trigger(self, message: str) -> str | None:
+        """Match a trigger in the message.
+
+        It returns the first trigger that matches the message.
+        """
+        message = message.lower()
+        for trigger in self.triggers:
+            # Check for exact match for triggers starting with "/"
+            if trigger.startswith('/') and trigger.lower() == message.strip().lower():
+                return trigger
+            # Otherwise, check for substring match
+            elif not trigger.startswith('/') and trigger.lower() in message:
+                return trigger
+        return None
+
+    @property
+    def triggers(self) -> list[str]:
+        return self.metadata.triggers
+
+    def extract_variables(self, content: str) -> List[str]:
+        """Extract variables from the content.
+
+        Variables are in the format ${variable_name}.
+        """
+        pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
+        matches = re.findall(pattern, content)
+        return matches
+
+    def requires_user_input(self) -> bool:
+        """Check if this microagent requires user input.
+
+        Returns True if the content contains variables in the format ${variable_name}.
+        """
+        # Check if the content contains any variables
+        variables = self.extract_variables(self.content)
+        return len(variables) > 0
+
+    @property
+    def inputs(self) -> List[InputMetadata]:
+        """Get the inputs for this microagent."""
+        return self.metadata.inputs
+
+
 def load_microagents_from_dir(
     microagent_dir: Union[str, Path],
 ) -> tuple[dict[str, RepoMicroagent], dict[str, KnowledgeMicroagent]]:
@@ -161,7 +264,7 @@ def load_microagents_from_dir(
         microagent_dir: Path to the microagents directory (e.g. .openhands/microagents)
 
     Returns:
-        Tuple of (repo_agents, knowledge_agents, task_agents) dictionaries
+        Tuple of (repo_agents, knowledge_agents) dictionaries
     """
     if isinstance(microagent_dir, str):
         microagent_dir = Path(microagent_dir)
@@ -182,6 +285,7 @@ def load_microagents_from_dir(
                 if isinstance(agent, RepoMicroagent):
                     repo_agents[agent.name] = agent
                 elif isinstance(agent, KnowledgeMicroagent):
+                    # Both KnowledgeMicroagent and TaskMicroagent go into knowledge_agents
                     knowledge_agents[agent.name] = agent
                 logger.debug(f'Loaded agent {agent.name} from {file}')
             except Exception as e:
diff --git a/openhands/microagent/types.py b/openhands/microagent/types.py
index fab42b7e0881..7b6cc0125523 100644
--- a/openhands/microagent/types.py
+++ b/openhands/microagent/types.py
@@ -1,4 +1,5 @@
 from enum import Enum
+from typing import List
 
 from pydantic import BaseModel, Field
 
@@ -8,6 +9,14 @@ class MicroagentType(str, Enum):
 
     KNOWLEDGE = 'knowledge'
     REPO_KNOWLEDGE = 'repo'
+    TASK = 'task'  # Special type for task microagents that require user input
+
+
+class InputMetadata(BaseModel):
+    """Metadata for task microagent inputs."""
+
+    name: str
+    description: str
 
 
 class MicroagentMetadata(BaseModel):
@@ -18,3 +27,4 @@ class MicroagentMetadata(BaseModel):
     version: str = Field(default='1.0.0')
     agent: str = Field(default='CodeActAgent')
     triggers: list[str] = []  # optional, only exists for knowledge microagents
+    inputs: List[InputMetadata] = []  # optional, only exists for task microagents
diff --git a/tests/unit/test_microagent_task.py b/tests/unit/test_microagent_task.py
new file mode 100644
index 000000000000..a397870f7de3
--- /dev/null
+++ b/tests/unit/test_microagent_task.py
@@ -0,0 +1,180 @@
+import tempfile
+
+from openhands.microagent.microagent import (
+    BaseMicroagent,
+    KnowledgeMicroagent,
+    TaskMicroagent,
+)
+from openhands.microagent.types import MicroagentType
+
+
+def test_task_microagent_creation():
+    """Test that a TaskMicroagent is created correctly."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent with a variable: ${test_var}.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert agent.type == MicroagentType.TASK
+        assert agent.name == 'test_task'
+        assert '/test_task' in agent.triggers
+        assert "If the user didn't provide any of these variables" in agent.content
+
+
+def test_task_microagent_variable_extraction():
+    """Test that variables are correctly extracted from the content."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+inputs:
+  - name: var1
+    description: "Variable 1"
+---
+
+This is a test with variables: ${var1}, ${var2}, and ${var3}.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        variables = agent.extract_variables(agent.content)
+        assert set(variables) == {'var1', 'var2', 'var3'}
+        assert agent.requires_user_input()
+
+
+def test_knowledge_microagent_no_prompt():
+    """Test that a regular KnowledgeMicroagent doesn't get the prompt."""
+    content = """---
+name: test_knowledge
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- test_knowledge
+---
+
+This is a test knowledge microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, KnowledgeMicroagent)
+        assert agent.type == MicroagentType.KNOWLEDGE
+        assert "If the user didn't provide any of these variables" not in agent.content
+
+
+def test_task_microagent_trigger_addition():
+    """Test that a trigger is added if not present."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert '/test_task' in agent.triggers
+
+
+def test_task_microagent_no_duplicate_trigger():
+    """Test that a trigger is not duplicated if already present."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+- another_trigger
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert agent.triggers.count('/test_task') == 1  # No duplicates
+        assert len(agent.triggers) == 2
+
+
+def test_task_microagent_match_trigger():
+    """Test that a task microagent matches its trigger correctly."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert agent.match_trigger('/test_task') == '/test_task'
+        assert (
+            agent.match_trigger('  /test_task  ') == '/test_task'
+        )  # Whitespace is trimmed
+        assert (
+            agent.match_trigger('This contains /test_task') is None
+        )  # Not an exact match
+        assert agent.match_trigger('/other_task') is None
diff --git a/tests/unit/test_task_microagent.py b/tests/unit/test_task_microagent.py
new file mode 100644
index 000000000000..a397870f7de3
--- /dev/null
+++ b/tests/unit/test_task_microagent.py
@@ -0,0 +1,180 @@
+import tempfile
+
+from openhands.microagent.microagent import (
+    BaseMicroagent,
+    KnowledgeMicroagent,
+    TaskMicroagent,
+)
+from openhands.microagent.types import MicroagentType
+
+
+def test_task_microagent_creation():
+    """Test that a TaskMicroagent is created correctly."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent with a variable: ${test_var}.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert agent.type == MicroagentType.TASK
+        assert agent.name == 'test_task'
+        assert '/test_task' in agent.triggers
+        assert "If the user didn't provide any of these variables" in agent.content
+
+
+def test_task_microagent_variable_extraction():
+    """Test that variables are correctly extracted from the content."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+inputs:
+  - name: var1
+    description: "Variable 1"
+---
+
+This is a test with variables: ${var1}, ${var2}, and ${var3}.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        variables = agent.extract_variables(agent.content)
+        assert set(variables) == {'var1', 'var2', 'var3'}
+        assert agent.requires_user_input()
+
+
+def test_knowledge_microagent_no_prompt():
+    """Test that a regular KnowledgeMicroagent doesn't get the prompt."""
+    content = """---
+name: test_knowledge
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- test_knowledge
+---
+
+This is a test knowledge microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, KnowledgeMicroagent)
+        assert agent.type == MicroagentType.KNOWLEDGE
+        assert "If the user didn't provide any of these variables" not in agent.content
+
+
+def test_task_microagent_trigger_addition():
+    """Test that a trigger is added if not present."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert '/test_task' in agent.triggers
+
+
+def test_task_microagent_no_duplicate_trigger():
+    """Test that a trigger is not duplicated if already present."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+- another_trigger
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert agent.triggers.count('/test_task') == 1  # No duplicates
+        assert len(agent.triggers) == 2
+
+
+def test_task_microagent_match_trigger():
+    """Test that a task microagent matches its trigger correctly."""
+    content = """---
+name: test_task
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /test_task
+inputs:
+  - name: TEST_VAR
+    description: "Test variable"
+---
+
+This is a test task microagent.
+"""
+
+    with tempfile.NamedTemporaryFile(suffix='.md') as f:
+        f.write(content.encode())
+        f.flush()
+
+        agent = BaseMicroagent.load(f.name)
+
+        assert isinstance(agent, TaskMicroagent)
+        assert agent.match_trigger('/test_task') == '/test_task'
+        assert (
+            agent.match_trigger('  /test_task  ') == '/test_task'
+        )  # Whitespace is trimmed
+        assert (
+            agent.match_trigger('This contains /test_task') is None
+        )  # Not an exact match
+        assert agent.match_trigger('/other_task') is None


Code review comments:
diff_hunk: @@ -0,0 +1,23 @@
+---
+name: update_pr_description
+version: 1.0.0
+author: openhands
+agent: CodeActAgent
+triggers:
+- /update_pr
path: microagents/update_pr_description.md
text: @user1:
```suggestion
- /pr_update
```