diff --git a/sdk/guides/task-tool-set.mdx b/sdk/guides/task-tool-set.mdx index 530f9354..afc42863 100644 --- a/sdk/guides/task-tool-set.mdx +++ b/sdk/guides/task-tool-set.mdx @@ -1,29 +1,95 @@ --- title: Task Tool Set -description: Delegate complex work to specialized sub-agents that run synchronously and return results to the parent agent. +description: Delegate complex work to specialized sub-agents with blocking or opt-in background execution. --- import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx"; -> A ready-to-run example is available [here](#ready-to-run-example)! +> A ready-to-run example is available [here](#ready-to-run-example). ## Overview -The TaskToolSet lets a parent agent launch sub-agents that handle complex, multi-step tasks autonomously. Each sub-agent runs **synchronously** — the parent blocks until the sub-agent finishes and returns its result. Sub-agents can be **resumed** later using a task ID, preserving their full conversation context. +The `TaskToolSet` lets a parent agent launch sub-agents that handle complex, +multi-step tasks autonomously. Blocking execution remains the default: the +parent waits for the sub-agent and receives its result. A task can instead opt +into process-local background execution, which returns a stable task ID +immediately. The parent can then poll or wait for output and request +cooperative cancellation through the companion lifecycle tools. This pattern is useful when: + - Delegating specialized work to purpose-built sub-agents -- Breaking a problem into sequential steps handled by different experts +- Running independent background tasks while the parent continues - Maintaining conversational context across multiple interactions with a sub-agent - Isolating sub-task complexity from the parent agent's context -TaskToolSet is designed for **sequential** blocking tasks. +Use the default blocking mode when the parent needs the result before +continuing. Set `run_in_background=True` only when the parent can continue +useful work while the sub-agent runs. +## Background Tasks + +`TaskToolSet` registers three related tools that share one task manager: +`task`, `task_output`, and `task_stop`. The latter two are used for tasks +started with `run_in_background=True`; blocking calls continue to use the same +`task` tool and existing resume behavior. + +Set `run_in_background=True` on a `TaskAction`. The launch observation returns +one stable `task_id`. Use `TaskOutputAction` to inspect the current state or +wait for a terminal result, and use `TaskStopAction` to request cooperative +cancellation. + +```python icon="python" +from openhands.tools.task import ( + TaskAction, + TaskOutputAction, + TaskStopAction, + TaskToolSet, +) + +# `conversation` is an existing LocalConversation with TaskToolSet enabled. +task_tools = TaskToolSet.create(conv_state=conversation.state) +task_executor = task_tools[0].executor + +started = task_executor( + TaskAction( + prompt="Review the authentication changes.", + subagent_type="code_reviewer", + run_in_background=True, + ), + conversation, +) +task_id = started.task_id + +status = task_executor(TaskOutputAction(task_id=task_id), conversation) +if status.status == "completed": + print(status.text) +elif status.status in {"queued", "running"}: + task_executor(TaskStopAction(task_id=task_id), conversation) +``` + +Background tasks move through `queued`, `running`, `completed`, `error`, or +`cancelled`. Multiple tasks can run at the same time. A second resume of the +same active task is rejected until the first task reaches a settled terminal +state. Repeated output reads are read-only, repeated stop requests are +idempotent for cancelled tasks, and neither operation duplicates usage metrics. + + +Background task IDs are process-local control handles owned by one task manager +and parent conversation. The registry is not restored after a process restart. +Persisted sub-agent conversation files may remain, but the current task API +reports the old ID as unknown because it does not restore workers or handles. + + ## How It Works -The agent calls the task tool with a prompt and a sub-agent type. The TaskManager creates (or resumes) a sub-agent conversation, runs it to completion, and returns the result to the parent. +The parent calls `task` with a prompt and a sub-agent type. The `TaskManager` +creates (or resumes) a sub-agent conversation. In blocking mode, the call +waits for the final response. In background mode, the call publishes a task and +returns while one managed worker uses `LocalConversation.arun()` and settles +output, errors, metrics, and cleanup exactly once. ```mermaid sequenceDiagram @@ -31,25 +97,25 @@ sequenceDiagram participant T as TaskManager participant S as Sub-Agent - P->>T: task(prompt, type) + P->>T: task(prompt, type, run_in_background=True) activate T - T->>S: create / resume + T->>S: create or resume conversation activate S - Note over S: runs autonomously - S->>T: result + T-->>P: task_id (queued or running) + P->>T: task_output(task_id) + S->>T: result, error, or interrupt deactivate S - T->>P: TaskObservation + T-->>P: terminal status and output deactivate T - Note right of T: persists for resume ``` ### Task Lifecycle -1. **Creation**: A fresh sub-agent and conversation are created -2. **Running**: The sub-agent processes the prompt autonomously -3. **Completion**: The final response is extracted and returned -4. **Persistence**: The conversation is saved to disk for potential resumption -5. **Resumption** (optional): A previous task can be resumed with its full context preserved +1. **Creation**: A fresh sub-agent conversation is created and assigned a task ID +2. **Queued / running**: Background mode returns while the worker runs asynchronously +3. **Completion**: The final response, error, or cancellation state is settled once +4. **Cleanup**: Metrics are recorded once and the sub-agent conversation is closed +5. **Resumption** (optional): A settled task can be resumed explicitly with its context preserved ## Setting Up the TaskToolSet @@ -57,11 +123,11 @@ sequenceDiagram ### Register Custom Sub-Agent Types (Optional) - By default, a `"default"` general-purpose agent is available, but you can register your own custom types - for specialized behavior: + The built-in `general-purpose` agent is available by default. You can + register custom types for specialized behavior: - ```python icon="python" focus={23-27} - from openhands.sdk import LLM, Agent, AgentContext + ```python icon="python" + from openhands.sdk import Agent, AgentContext, LLM from openhands.sdk.context import Skill from openhands.sdk.subagent import register_agent @@ -73,10 +139,7 @@ sequenceDiagram skills=[ Skill( name="code_review", - content="""You are an expert code reviewer. - Analyze code for bugs, style issues, - and suggest improvements. - """, + content="Review code for correctness and regressions.", trigger=None, ) ], @@ -86,14 +149,14 @@ sequenceDiagram register_agent( name="code_reviewer", factory_func=create_code_reviewer, - description="Reviews code for bugs, style issues, and improvements.", + description="Reviews code for bugs and regressions.", ) ``` ### Add TaskToolSet to the Agent - ```python icon="python" focus={6} + ```python icon="python" from openhands.sdk import Agent, Tool from openhands.tools.task import TaskToolSet @@ -103,15 +166,18 @@ sequenceDiagram ) ``` - The tool auto-registers on import — no explicit `register_tool()` call is needed. + The tool set is registered on import; no explicit `register_tool()` + call is needed. Resolving `TaskToolSet` creates `task`, `task_output`, + and `task_stop` with one shared executor. ### Create a Conversation - ```python icon="python" focus={5-9} + ```python icon="python" + from pathlib import Path + from openhands.sdk import Conversation from openhands.tools.delegate import DelegationVisualizer - from pathlib import Path conversation = Conversation( agent=agent, @@ -120,51 +186,54 @@ sequenceDiagram ) ``` - - The `DelegationVisualizer` is optional but recommended — it shows the multi-agent conversation flow in the terminal. - + The visualizer is optional; when provided, it shows the parent and + sub-agent conversation flow. ## Tool Parameters -When the parent agent calls the task tool, it provides these parameters: - | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `prompt` | `str` | Yes | The instruction for the sub-agent | -| `subagent_type` | `str` | No | Which registered agent type to use (default: `"default"`) | -| `description` | `str` | No | Short label (3-5 words) for display and tracking | -| `resume` | `str` | No | Task ID from a previous invocation to continue | +| `subagent_type` | `str` | No | Registered agent type; the legacy default name is `"default"` and resolves to `general-purpose` | +| `description` | `str` | No | Short label for display and tracking | +| `resume` | `str` | No | Settled task ID to continue | +| `run_in_background` | `bool` | No | Return a task ID immediately; defaults to `False` | + +For `task_output`, `block=True` waits for a terminal state, timeout, or a +stop/manager-close signal so a reader is not stranded during cooperative +cleanup. Its `timeout` must be between `0` and `3600` seconds and must be +finite. `task_stop` requests cooperative cancellation through the child +`LocalConversation`; it never force-kills a Python thread. ## Task Observation -The tool returns a `TaskObservation` containing: +The task tools return observations containing: | Field | Description | |-------|-------------| -| `task_id` | Unique identifier (e.g., `task_00000001`) — use this for resumption | -| `subagent` | The agent type that handled the task | -| `status` | Final status: `completed` or `error` | -| `text` | The sub-agent's response (or error message) | +| `task_id` | Stable identifier returned by `task` | +| `subagent` | Agent type assigned to the task | +| `status` | `queued`, `running`, `completed`, `error`, or `cancelled` | +| `text` | Response, partial cancellation output, or error message | + +Unknown task IDs and invalid lifecycle operations return an error observation +without changing another task. Reading status or output repeatedly is safe and +does not duplicate usage accounting. ## Resuming Tasks -A key feature of TaskToolSet is the ability to resume a previously completed task. When a task finishes, its conversation is persisted to disk. Passing the `resume` parameter with the task ID reloads the full conversation history, allowing the sub-agent to continue where it left off. +Within the same manager lifetime, a settled task's conversation is persisted to +disk. Passing its `task_id` as `resume` reloads the full conversation history, +allowing the sub-agent to continue where it left off. Resuming while a task is +queued, running, or still settling is rejected. A process restart does not +restore the in-memory task registry or running workers, so the current task API +reports the old ID as unknown even when conversation files remain. ```python icon="python" -# First call — sub-agent generates a quiz question -conversation.send_message( - "Use the task tool with subagent_type='quiz_expert' to generate " - "a multiple-choice question about zebras." -) -conversation.run() -# The agent receives task_id "task_00000001" in the observation - -# Second call — resume the same sub-agent to verify the answer conversation.send_message( - "The user answered A. Use the task tool with resume='task_00000001' " - "to ask the same sub-agent whether that answer is correct." + "Use the task tool with resume='task_00000001' to verify the previous result." ) conversation.run() ``` @@ -172,135 +241,13 @@ conversation.run() ## Ready-to-run Example -This example is available on GitHub: [examples/01_standalone_sdk/41_task_tool_set.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py) +This example is available on GitHub: +[examples/01_standalone_sdk/41_task_tool_set.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py) -```python icon="python" expandable examples/01_standalone_sdk/40_task_tool_set.py -""" -Animal Quiz with Task Tool Set - -Demonstrates the TaskToolSet with a main agent delegating to an -animal-expert sub-agent. The flow is: - -1. User names an animal. -2. Main agent delegates to the "animal_expert" sub-agent to generate - a multiple-choice question about that animal. -3. Main agent shows the question to the user. -4. User picks an answer. -5. Main agent resumes the same sub-agent to check whether the answer - is correct and explain why. -""" - -import os - -from pydantic import SecretStr - -from openhands.sdk import LLM, Agent, AgentContext, Conversation, Tool -from openhands.sdk.context import Skill -from openhands.sdk.subagent import register_agent -from openhands.tools.delegate import DelegationVisualizer -from openhands.tools.task import TaskToolSet - - -# ── LLM setup ──────────────────────────────────────────────────────── - -api_key = os.getenv("LLM_API_KEY") -assert api_key is not None, "LLM_API_KEY environment variable is not set." - -llm = LLM( - model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), - api_key=SecretStr(api_key), - base_url=os.getenv("LLM_BASE_URL", None), -) - -# ── Register the animal expert sub-agent ───────────────────────────── - - -def create_animal_expert(llm: LLM) -> Agent: - """Factory for the animal-expert sub-agent.""" - return Agent( - llm=llm, - tools=[], # no tools needed – pure knowledge - agent_context=AgentContext( - skills=[ - Skill( - name="animal_expertise", - content=( - "You are a world-class zoologist. " - "When asked to generate a quiz question, respond with " - "EXACTLY this format and nothing else:\n\n" - "Question: \n" - "A)