feat: stream sub-task (child session) events through the bridge#110
feat: stream sub-task (child session) events through the bridge#110ColeMurray merged 4 commits intomainfrom
Conversation
When OpenCode's LLM calls the `task` tool (sub-agent), it creates a child session whose events were silently dropped by the bridge's session filter. Users saw "task tool running" → long silence → final result. Stream child session tool calls, step markers, and errors with `isSubtask: true` so the control plane can forward them to clients. Child text tokens are suppressed to avoid corrupting the web UI's single-ref text display. - Track child sessions via `session.created` (parentID match) and task tool metadata (covers resumed sessions with no `session.created`) - Session-scoped tool dedupe keys prevent parent/child callID collisions - Child errors forwarded without terminating the parent stream - Child idle/status events naturally ignored by existing parent guards - Extract `_extract_error_message()` helper to deduplicate error parsing
Terraform Validation Results
Pushed by: @ColeMurray, Action: |
Greptile OverviewGreptile SummaryAdds streaming support for sub-task (child session) events to prevent silent periods during sub-agent execution. The implementation tracks child sessions via dual discovery ( Key observations:
|
Terraform Validation Results
Pushed by: @ColeMurray, Action: |
There was a problem hiding this comment.
Pull request overview
This PR fixes a gap in the OpenCode SSE bridge where child session (sub-task) events were being filtered out, causing silent periods during sub-agent execution. It adds child session discovery and forwarding of non-text subtask events (with isSubtask: true) while suppressing child text tokens to avoid UI corruption.
Changes:
- Track direct child sessions via
session.createdand resumed child sessions viatasktoolmetadata.sessionId. - Forward child session tool/step/error events through the bridge with
isSubtask: true, while suppressing child text tokens. - Add test coverage for error-message extraction and subtask streaming behavior (including buffering and callID collision cases).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/modal-infra/src/sandbox/bridge.py | Implements child session tracking, subtask event forwarding, tool dedupe scoping by session, and _extract_error_message() helper. |
| packages/modal-infra/tests/test_bridge_sse.py | Adds tests for _extract_error_message() and subtask streaming (forwarding, suppression, buffering, and collision handling). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elif error_session_id in tracked_child_session_ids: | ||
| error_msg = self._extract_error_message(props.get("error", {})) | ||
| self.log.warn( | ||
| "bridge.child_session_error", | ||
| error_msg=error_msg, | ||
| child_session_id=error_session_id, | ||
| ) | ||
| yield { | ||
| "type": "error", | ||
| "error": error_msg or "Sub-task error", | ||
| "messageId": message_id, | ||
| "isSubtask": True, | ||
| } |
There was a problem hiding this comment.
Child session.error events are forwarded as {type: 'error', isSubtask: true}. In _handle_prompt(), any event with type == 'error' currently flips had_error=True and will make the final execution_complete.success false even if the parent stream recovers and completes normally. If the intent is “child errors don’t fail the parent execution”, consider emitting a different event type for subtask errors (or adjusting the parent error accounting to ignore isSubtask errors).
| metadata = part.get("metadata") or {} | ||
| child_sid = metadata.get("sessionId") |
There was a problem hiding this comment.
metadata = part.get('metadata') or {} assumes metadata is a dict; if OpenCode ever sends metadata as a non-dict truthy value (e.g., string/list), metadata.get(...) will raise and abort streaming. Consider guarding with isinstance(metadata, dict) before accessing .get() to make the bridge resilient to schema changes.
| metadata = part.get("metadata") or {} | |
| child_sid = metadata.get("sessionId") | |
| metadata = part.get("metadata") | |
| if isinstance(metadata, dict): | |
| child_sid = metadata.get("sessionId") | |
| else: | |
| child_sid = None |
… format - Change child session.error log level from warn to error for consistency - Add test_grandchild_session_not_tracked to validate direct-children-only boundary - Apply ruff formatting to both changed files
Terraform Validation Results
Pushed by: @ColeMurray, Action: |
Add isinstance(metadata, dict) check before accessing .get() on task tool metadata, preventing AttributeError if metadata is ever a non-dict truthy value.
Terraform Validation Results
Pushed by: @ColeMurray, Action: |
Summary
tasktool (sub-agent), child session events were silently dropped by the bridge's session filter, causing users to see long silences during sub-task executionisSubtask: truefor future UI differentiationKey design decisions
session.createdevents (common case) + task toolmetadata.sessionId(covers resumed sessions viatask_id)tool:{sessionID}:{callID}:{status}prevents parent/child callID collisionsisSubtask: truebut parent stream continuestaskpermission on sub-agents, which is denied by default — YAGNI for nowisSubtaskflows through as generic JSONChanges
bridge.pyhandle_part()withis_subtask, expand session filter, route child events, extract_extract_error_message()helpertest_bridge_sse.py_extract_error_message, 8 forTestSubtaskStreaming(tool forwarding, text suppression, idle/status non-termination, error forwarding, buffering race, metadata discovery, callID collision)Test plan
tasktool invocation in a live session and confirm child tool events appear in the web UI event stream