OpenHands / OpenHands/software-agent-sdk

feat: forward sub-agent (TaskToolSet) inner events to the live conversation stream

Open
#3,907 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

TL;DR — Sub-agents spawned via TaskToolSet run in their own LocalConversation. Their inner events never reach the parent's live event stream, so any real-time consumer (e.g. the agent-server WebSocket) goes blind the moment a sub-agent starts working. This proposes an opt-in, default-off forwarding path that streams sub-agent events to the parent's pub/sub — tagged for correlation, and without touching the main agent's LLM context. Implemented and validated end-to-end; PRs linked below.

Current behavior

When the main agent delegates via the task tool (TaskToolSet), the sub-agent runs in its own LocalConversation. Its inner actions/observations (terminal, file_editor, etc.) are persisted to the sub-agent's on-disk store, but only the TaskAction and the final TaskObservation are ever published to the parent conversation's event stream.

Concretely, TaskManager (openhands-tools/openhands/tools/task/manager.py) drives the sub-conversation in isolation and returns only the summarized observation upward. There is no path from the sub-agent's event stream to the parent's pub/sub.

Why this matters

Anything consuming the parent's live stream — most importantly the agent-server WebSocket /sockets/events/{conversation_id} — cannot show what a sub-agent is doing while it runs. From the user's perspective the agent appears frozen during delegation (which can be the longest part of a turn). Today's only workaround is polling the sub-agent's subagents/<id>/events/ directory, which is racy, off-stream, and not available to remote WebSocket clients at all.

This is a general UX/observability gap for any front-end built on the live stream, not specific to one deployment.

Concrete adopter motivation. We're building a platform on top of OpenHands with a chat interface, and surfacing sub-agent activity in that UI in real time is a core requirement — users need to see what a delegated sub-agent is doing, not just a frozen "delegating…" state followed by a summary.

Ecosystem parity. Peer agent SDKs already expose this. Anthropic's Claude Agent SDK emits sub-agent events tagged with parent_tool_use_id (which this proposal deliberately mirrors), and OpenAI's agent/streaming APIs likewise surface nested sub-agent activity. Consumers of those SDKs can already render sub-agent work in their chat UIs; OpenHands consumers currently cannot. Closing this gap brings OpenHands to parity and unblocks platforms that want to build a first-class delegation view on top of it.

Expected behavior

Sub-agent inner events should be observable on the parent's live stream in real time, while:

  1. the main agent's prompt/context stays byte-for-byte unchanged (delegation isolation preserved), and
  2. existing consumers that don't opt in see identical behavior to today.

Proposed design

  1. Event.parent_tool_use_id: str | None (new field, default None) — mirrors Anthropic's Claude Agent SDK. None = main-agent event; non-null = sub-agent event, value = the tool_call_id of the TaskAction that spawned it. (openhands-sdk/openhands/sdk/event/base.py)
  2. Forward, don't persist into the parent. When a sub-agent emits an event, stamp parent_tool_use_id and publish it to the parent's pub/subnot appended to the parent's state.events. This reuses the existing "publish-to-pubsub-but-not-persist" path already used for StreamingDeltaEvent, so the main agent's context is unaffected. (task/manager.py, conversation/state.py, tool/tool.py)
  3. Flag-gated, default off: OH_FORWARD_SUBAGENT_EVENTS (agent-server config) — behavior is identical to today unless explicitly enabled. (agent_server/config.py, agent_server/event_service.py)

Consumers correlate by nesting: parent_tool_use_id == null → main timeline; otherwise nest under the task whose tool_call_id matches.

Scope & touchpoints (two repos)

software-agent-sdk (this repo) — field, forwarding callback, sink wiring, flag:

  • openhands-sdk/openhands/sdk/event/base.pyparent_tool_use_id field
  • openhands-sdk/openhands/sdk/conversation/state.py — sub-event sink
  • openhands-sdk/openhands/sdk/tool/tool.py, openhands-sdk/openhands/sdk/agent/agent.py — thread the parent tool_call_id
  • openhands-tools/openhands/tools/task/{manager,impl,definition}.py — forwarding callback
  • openhands-agent-server/openhands/agent_server/{config,event_service}.pyOH_FORWARD_SUBAGENT_EVENTS flag + publish-not-persist sink

OpenHands (app-server) — receive tagged events, store/serve on a separate sub-stream:

  • openhands/app_server/event/{event_service,event_service_base,subagent_event_router}.py
  • openhands/app_server/event_callback/webhook_router.py, openhands/app_server/v1_router.py

Acceptance criteria

  • With OH_FORWARD_SUBAGENT_EVENTS off, the parent live stream and the main agent's context are unchanged from today (no regression).
  • With the flag on, sub-agent inner events appear on the parent's live stream in real time, each carrying the spawning TaskAction's tool_call_id in parent_tool_use_id.
  • The parent's persisted state.events and the main agent's prompt are not altered by forwarding.
  • App-server stores/serves forwarded events on a separate sub-stream, keeping the parent's flat store clean.
  • Works for both built-in and plugin-provided sub-agents.

Validation

Prototyped end-to-end against a local fork. With the flag on, sub-agent inner events stream live on the agent-server WebSocket, each tagged with the parent tool_call_id, while the parent stream still shows only TaskAction + TaskObservation and the main agent's context is unaffected. Verified for both a built-in sub-agent (code-explorer) and a plugin-provided sub-agent (code-reviewer). Unit coverage accompanies both PRs.

Implementation status

Up as two PRs (drafts pending agreement on the approach here):

  • SDK (core): OpenHands/software-agent-sdk#3908
  • OpenHands (app-server): OpenHands/OpenHands#15020 — depends on #3908

Open questions for maintainers

  1. Is parent_tool_use_id on the base Event the preferred correlation field (vs. a dedicated wrapper/event type)?
  2. Is reusing the StreamingDeltaEvent publish-not-persist mechanism the right pattern, or is there a preferred channel?
  3. Naming/placement of the feature flag (OH_FORWARD_SUBAGENT_EVENTS)?
  4. Should this stay opt-in (default off) long-term, or eventually default-on?

Happy to rebase #3908 onto main and take it out of draft once the approach gets a thumbs-up.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with openhands-sdk/openhands/sdk/event/base.py and the TaskManager files, then trace the state, tool, agent-server, and app-server event paths listed in the scope. Review the draft PRs #3908 and #15020 and their accompanying unit coverage before changing anything. Done means the acceptance criteria hold for built-in and plugin sub-agents without altering the parent context or persisted events.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, distributed-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.