From 7a78f4a58643a1b9849dfca295463200a359f203 Mon Sep 17 00:00:00 2001 From: KirschBluteX <114209152+KirschBluteX@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:31:20 -0700 Subject: [PATCH 1/3] docs(sdk): document background delegation Co-authored-by: openhands --- sdk/guides/task-tool-set.mdx | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/sdk/guides/task-tool-set.mdx b/sdk/guides/task-tool-set.mdx index 530f9354c..488df4c8a 100644 --- a/sdk/guides/task-tool-set.mdx +++ b/sdk/guides/task-tool-set.mdx @@ -21,6 +21,80 @@ This pattern is useful when: TaskToolSet is designed for **sequential** blocking tasks. +## Background DelegateExecutor + +`TaskToolSet` remains blocking and supports resuming a completed task. Use the +lower-level `DelegateExecutor` when a parent must continue working while one or +more named sub-agents run in the background. + +Set `background=True` on a `delegate` action. The observation returns one stable +task ID for each sub-agent in the `tasks` map. Use lifecycle actions to check the +status, retrieve output, or stop the task. + +```python icon="python" +from openhands.tools.delegate import ( + DelegateAction, + DelegateExecutor, + DelegateTaskStatus, +) + +delegate = DelegateExecutor() + +# Spawn a named conversation. "code_reviewer" was registered above. +delegate( + DelegateAction( + command="spawn", + ids=["reviewer"], + agent_types=["code_reviewer"], + ), + conversation, +) + +started = delegate( + DelegateAction( + command="delegate", + tasks={"reviewer": "Review the authentication changes."}, + background=True, + ), + conversation, +) +assert started.task_ids is not None +task_id = started.task_ids["reviewer"] + +status = delegate( + DelegateAction(command="status", task_id=task_id), + conversation, +) +if status.status == DelegateTaskStatus.COMPLETED: + output = delegate( + DelegateAction(command="output", task_id=task_id), + conversation, + ) + print(output.text) +elif status.status in { + DelegateTaskStatus.QUEUED, + DelegateTaskStatus.RUNNING, +}: + # Request cooperative cancellation when the result is no longer needed. + delegate( + DelegateAction(command="stop", task_id=task_id), + conversation, + ) +``` + +Background tasks move through `queued`, `running`, `completed`, `failed`, or +`cancelled`. Multiple sub-agents can run at the same time, but a second task for +the same spawned agent is rejected until its active task reaches a terminal +state. Reading status or output more than once is safe and does not duplicate +usage metrics. + + +Background task IDs are in-process control handles owned by one +`DelegateExecutor` and parent conversation. The registry is not restored after a +process restart. Persisted sub-agent conversation data does not restore running +workers or their task IDs. + + ## 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. From aa18a51ded29370606e032d3a952682f4339c826 Mon Sep 17 00:00:00 2001 From: KirschBluteX <114209152+KirschBluteX@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:46:07 -0700 Subject: [PATCH 2/3] docs(sdk): clarify background setup ordering Co-authored-by: openhands --- sdk/guides/task-tool-set.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/guides/task-tool-set.mdx b/sdk/guides/task-tool-set.mdx index 488df4c8a..5d06f3d97 100644 --- a/sdk/guides/task-tool-set.mdx +++ b/sdk/guides/task-tool-set.mdx @@ -40,7 +40,8 @@ from openhands.tools.delegate import ( delegate = DelegateExecutor() -# Spawn a named conversation. "code_reviewer" was registered above. +# Assume conversation already exists and "code_reviewer" is registered as +# shown later on this page. delegate( DelegateAction( command="spawn", From ce536f6087b01c1eb1bc2b7c7451430fab020f24 Mon Sep 17 00:00:00 2001 From: KirschBluteX <114209152+KirschBluteX@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:05:03 -0700 Subject: [PATCH 3/3] docs(sdk): document TaskToolSet background tasks Co-authored-by: openhands --- sdk/guides/task-tool-set.mdx | 352 +++++++++++------------------------ 1 file changed, 112 insertions(+), 240 deletions(-) diff --git a/sdk/guides/task-tool-set.mdx b/sdk/guides/task-tool-set.mdx index 5d06f3d97..afc428631 100644 --- a/sdk/guides/task-tool-set.mdx +++ b/sdk/guides/task-tool-set.mdx @@ -1,104 +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 DelegateExecutor +## Background Tasks -`TaskToolSet` remains blocking and supports resuming a completed task. Use the -lower-level `DelegateExecutor` when a parent must continue working while one or -more named sub-agents run in the background. +`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 `background=True` on a `delegate` action. The observation returns one stable -task ID for each sub-agent in the `tasks` map. Use lifecycle actions to check the -status, retrieve output, or stop the task. +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.delegate import ( - DelegateAction, - DelegateExecutor, - DelegateTaskStatus, +from openhands.tools.task import ( + TaskAction, + TaskOutputAction, + TaskStopAction, + TaskToolSet, ) -delegate = DelegateExecutor() +# `conversation` is an existing LocalConversation with TaskToolSet enabled. +task_tools = TaskToolSet.create(conv_state=conversation.state) +task_executor = task_tools[0].executor -# Assume conversation already exists and "code_reviewer" is registered as -# shown later on this page. -delegate( - DelegateAction( - command="spawn", - ids=["reviewer"], - agent_types=["code_reviewer"], +started = task_executor( + TaskAction( + prompt="Review the authentication changes.", + subagent_type="code_reviewer", + run_in_background=True, ), conversation, ) +task_id = started.task_id -started = delegate( - DelegateAction( - command="delegate", - tasks={"reviewer": "Review the authentication changes."}, - background=True, - ), - conversation, -) -assert started.task_ids is not None -task_id = started.task_ids["reviewer"] - -status = delegate( - DelegateAction(command="status", task_id=task_id), - conversation, -) -if status.status == DelegateTaskStatus.COMPLETED: - output = delegate( - DelegateAction(command="output", task_id=task_id), - conversation, - ) - print(output.text) -elif status.status in { - DelegateTaskStatus.QUEUED, - DelegateTaskStatus.RUNNING, -}: - # Request cooperative cancellation when the result is no longer needed. - delegate( - DelegateAction(command="stop", task_id=task_id), - conversation, - ) +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`, `failed`, or -`cancelled`. Multiple sub-agents can run at the same time, but a second task for -the same spawned agent is rejected until its active task reaches a terminal -state. Reading status or output more than once is safe and does not duplicate -usage metrics. +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 in-process control handles owned by one -`DelegateExecutor` and parent conversation. The registry is not restored after a -process restart. Persisted sub-agent conversation data does not restore running -workers or their task IDs. +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 @@ -106,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 @@ -132,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 @@ -148,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, ) ], @@ -161,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 @@ -178,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, @@ -195,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() ``` @@ -247,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)