OpenHands / OpenHands/software-agent-sdk
feat: forward sub-agent (TaskToolSet) inner events to the live conversation stream
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
TL;DR — Sub-agents spawned via
TaskToolSetrun in their ownLocalConversation. 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:
- the main agent's prompt/context stays byte-for-byte unchanged (delegation isolation preserved), and
- existing consumers that don't opt in see identical behavior to today.
Proposed design
Event.parent_tool_use_id: str | None(new field, defaultNone) — mirrors Anthropic's Claude Agent SDK.None= main-agent event; non-null = sub-agent event, value = thetool_call_idof theTaskActionthat spawned it. (openhands-sdk/openhands/sdk/event/base.py)- Forward, don't persist into the parent. When a sub-agent emits an event, stamp
parent_tool_use_idand publish it to the parent's pub/sub — not appended to the parent'sstate.events. This reuses the existing "publish-to-pubsub-but-not-persist" path already used forStreamingDeltaEvent, so the main agent's context is unaffected. (task/manager.py,conversation/state.py,tool/tool.py) - 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.py—parent_tool_use_idfieldopenhands-sdk/openhands/sdk/conversation/state.py— sub-event sinkopenhands-sdk/openhands/sdk/tool/tool.py,openhands-sdk/openhands/sdk/agent/agent.py— thread the parenttool_call_idopenhands-tools/openhands/tools/task/{manager,impl,definition}.py— forwarding callbackopenhands-agent-server/openhands/agent_server/{config,event_service}.py—OH_FORWARD_SUBAGENT_EVENTSflag + 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}.pyopenhands/app_server/event_callback/webhook_router.py,openhands/app_server/v1_router.py
Acceptance criteria
- With
OH_FORWARD_SUBAGENT_EVENTSoff, 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'stool_call_idinparent_tool_use_id. - The parent's persisted
state.eventsand 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
- Is
parent_tool_use_idon the baseEventthe preferred correlation field (vs. a dedicated wrapper/event type)? - Is reusing the
StreamingDeltaEventpublish-not-persist mechanism the right pattern, or is there a preferred channel? - Naming/placement of the feature flag (
OH_FORWARD_SUBAGENT_EVENTS)? - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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