Skip to content

Conversation

@ColeMurray
Copy link
Owner

Summary

  • When OpenCode's LLM calls the task tool (sub-agent), child session events were silently dropped by the bridge's session filter, causing users to see long silences during sub-task execution
  • Stream child session tool calls, step markers, and errors through the bridge with isSubtask: true for future UI differentiation
  • Child text tokens are suppressed to avoid corrupting the web UI's single-ref text display

Key design decisions

  • Dual child discovery: session.created events (common case) + task tool metadata.sessionId (covers resumed sessions via task_id)
  • Session-scoped tool dedupe: tool:{sessionID}:{callID}:{status} prevents parent/child callID collisions
  • Child errors don't terminate: forwarded with isSubtask: true but parent stream continues
  • Direct children only (v1): nested sub-tasks require explicit task permission on sub-agents, which is denied by default — YAGNI for now
  • No control plane type changes: isSubtask flows through as generic JSON

Changes

File What
bridge.py Track child sessions, parameterize handle_part() with is_subtask, expand session filter, route child events, extract _extract_error_message() helper
test_bridge_sse.py 14 new tests: 6 for _extract_error_message, 8 for TestSubtaskStreaming (tool forwarding, text suppression, idle/status non-termination, error forwarding, buffering race, metadata discovery, callID collision)

Test plan

  • All 14 new tests pass
  • All 119 existing tests pass (0 regressions)
  • Manual verification: trigger a task tool invocation in a live session and confirm child tool events appear in the web UI event stream

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
@ColeMurray ColeMurray requested a review from Copilot February 12, 2026 08:41
@github-actions
Copy link

Terraform Validation Results

Step Status
Format ⚠️
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@greptile-apps
Copy link

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

Adds streaming support for sub-task (child session) events to prevent silent periods during sub-agent execution. The implementation tracks child sessions via dual discovery (session.created events and task tool metadata), forwards non-text events with isSubtask: true, and ensures child errors don't terminate the parent stream.

Key observations:

  • Session-scoped tool deduplication (tool:{sessionID}:{callID}:{status}) correctly prevents callID collisions between parent/child sessions
  • Child text suppression avoids corrupting the web UI's single-ref display
  • Buffering logic handles race conditions where child parts arrive before message.updated
  • Comprehensive test coverage (14 new tests) validates tool forwarding, text suppression, non-terminating child idle/status/error events, buffering races, metadata discovery, and callID collision handling

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@github-actions
Copy link

Terraform Validation Results

Step Status
Format ⚠️
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.created and resumed child sessions via task tool metadata.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.

Comment on lines 1016 to 1028
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,
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 943 to 944
metadata = part.get("metadata") or {}
child_sid = metadata.get("sessionId")
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
… 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
@github-actions
Copy link

Terraform Validation Results

Step Status
Format ⚠️
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

Add isinstance(metadata, dict) check before accessing .get() on task
tool metadata, preventing AttributeError if metadata is ever a non-dict
truthy value.
@github-actions
Copy link

Terraform Validation Results

Step Status
Format ⚠️
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@ColeMurray ColeMurray merged commit 64afd1a into main Feb 12, 2026
10 checks passed
@ColeMurray ColeMurray deleted the feat/subtask-streaming branch February 12, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant