Skip to content

feat(app-conversation): launch child conversations server-side for Cloud parents - #433

Open
hieptl wants to merge 1 commit into
mainfrom
hieptl/ohe-3176
Open

hieptl wants to merge 1 commit into
mainfrom
hieptl/ohe-3176

Conversation

@hieptl

@hieptl hieptl commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

HUMAN:

A Cloud parent has no way to start a child conversation today: Agent Canvas only registers its browser-executed launch tool for parents on a local agent server, and the Cloud start request cannot carry client tools. This adds the Cloud launcher for the SDK's start_child_conversation tool, so the child goes through the normal app-conversation start and is linked to its parent. Validated with the unit tests and the CI-parity hooks against the SDK branch in OpenHands/software-agent-sdk#5126; the dev-stack walkthrough below is still to be done.

  • A human has tested these changes.

AGENT:


Why

Agent Canvas launches child conversations from the browser through a client-defined tool. That makes a side-effecting, billable operation depend on browser event delivery and on a per-browser localStorage ledger to avoid duplicates, and it does not work for Cloud at all: AppConversationStartRequest has no client_tools, so a Cloud-hosted parent never gets the tool.

OpenHands/software-agent-sdk#5126 adds a server-executed start_child_conversation tool that posts {task, title, isolation} to a configurable launch_url and returns the child's conversation_id, status and url to the agent. Children of a Cloud parent should run on the same Cloud backend and go through the normal lifecycle (sandbox, repository, credentials, quota), so the launcher has to live here rather than in the sandbox's agent server.

The app server already supports sub-conversations end to end: parent_conversation_id on the start request, _inherit_configuration_from_parent (sandbox, repository, branch, provider, model), the indexed column, sub_conversation_ids, cascade delete and list filtering. What was missing is a way for the agent inside the sandbox to trigger that start without a browser.

Summary

  • New POST /api/v1/webhooks/conversations/{conversation_id}/children in webhook_router.py. It authenticates with the sandbox session key through the existing valid_sandbox dependency (the /api/v1/webhooks/ prefix is already exempt from the SaaS auth middleware), returns 404 for an unknown parent and rejects a parent owned by another user or sandbox. It then runs start_app_conversation to completion as the parent's owner, with parent_conversation_id and the task as the initial message, scoped to the parent's organization through set_effective_org_id_override (resolved from conversation_metadata_saas, a no-op outside SaaS). It returns StartChildConversationResponse with the child id, start-task status and {web_url}/conversations/{id}; a failed start comes back as a 500 carrying the start task's detail.
  • _build_start_conversation_request_for_user attaches Tool(name="start_child_conversation", params={"launch_url": "{web_url}/api/v1/webhooks/conversations/{conversation_id}/children"}) to non-planner OpenHands agents when web_url is configured. Planner and ACP agents are unchanged.
  • Tests: tests/unit/app_server/test_webhook_router_child_conversation.py (6) and two cases in test_live_status_app_conversation_service.py.

Issue Number

Fixes #278
Linear: https://linear.app/all-hands-ai/issue/OHE-3176/support-server-side-same-backend-child-conversation-launches-in-cloud
Depends on: OpenHands/software-agent-sdk#5126 (OHE-3232)

How to Test

  1. Unit tests and type checking. The pinned openhands-sdk 1.47.0 does not ship the tool yet, so install the SDK branch from feat(agent-server): add start_child_conversation tool with a pluggable launcher software-agent-sdk#5126 over the synced venv. Docker needs to be up for the Postgres testcontainer that the root conftest.py starts.

    uv sync --all-groups --frozen
    uv pip install --no-deps \
      -e ../software-agent-sdk/openhands-sdk -e ../software-agent-sdk/openhands-tools \
      -e ../software-agent-sdk/openhands-workspace -e ../software-agent-sdk/openhands-agent-server
    uv run --no-sync pytest -q tests/unit/app_server/test_webhook_router_child_conversation.py
    # 6 passed
    uv run --no-sync pytest -q tests/unit/app_server/test_live_status_app_conversation_service.py -k child_conversation_launcher
    # 2 passed
    uv run --no-sync pytest -q tests/unit/app_server/test_webhook_router_child_conversation.py \
      tests/unit/app_server/test_webhook_router_parent_conversation.py \
      tests/unit/app_server/test_live_status_app_conversation_service.py
    # 211 passed
    uv run --no-sync pre-commit run --config ./dev_config/python/.pre-commit-config.yaml --files \
      openhands/app_server/event_callback/webhook_router.py \
      openhands/app_server/app_conversation/live_status_app_conversation_service.py \
      tests/unit/app_server/test_webhook_router_child_conversation.py \
      tests/unit/app_server/test_live_status_app_conversation_service.py
    # all hooks pass, incl. mypy

    The webhook tests pin that one call provisions exactly one child whose start request carries the parent id, title and task; that the launch is scoped to the parent's organization; that the response carries the child id, READY status and URL; that an unknown parent is 404 with nothing started; that a parent from another sandbox raises AuthError with nothing started; and that an ERROR start task surfaces as a 500 with its detail. The service tests pin the injected tool and its launch_url, and that nothing is attached without web_url.

  2. Dev-stack walkthrough, with a sandbox image built from the SDK branch:

    1. Start a Cloud conversation on a repository.
    2. Ask the agent to hand a self-contained task to a child with start_child_conversation.
    3. The tool observation shows the child conversation_id, status READY and a /conversations/<id> link.
    4. GET /api/v1/app-conversations?ids=<child> shows parent_conversation_id set to the parent, the same sandbox_id, and the parent's repository and branch; the parent's sub_conversation_ids lists the child.
    5. The child starts with every Canvas tab closed, since nothing in the browser is involved.

Video/Screenshots

To be added with the dev-stack walkthrough.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • CI depends on the SDK. openhands.tools.child_conversation and the request/response models are not in openhands-sdk 1.47.0, so imports of webhook_router fail in CI until feat(agent-server): add start_child_conversation tool with a pluggable launcher software-agent-sdk#5126 ships in a release and the pins in pyproject.toml / uv.lock are bumped on this branch.
  • Rollout order: SDK release, then the pin bump, then this change. The tool must not be attached before the sandbox image carries it, or conversation start fails resolving Tool(name="start_child_conversation"). Default sandboxes follow the bundled agent-server version, so they move in lockstep; custom images that lag would hit that failure.
  • SDK alternative. feat(agent-server): launch same-backend child conversations software-agent-sdk#4890 implements the same SDK issue differently: an in-process launch_child_conversation tool attached to every native agent, with no way to route the launch through the host. On Cloud that would create children inside the sandbox outside this lifecycle, and on_conversation_update would record them without the parent link, because it only preserves an existing parent_conversation_id. If that shape is chosen instead, the launcher here still applies but the SDK side needs a seam to reach it.
  • Inline start. The endpoint drives start_app_conversation to READY within the tool call (the SDK tool times out after 300 s) and calls back into the same sandbox agent server while the parent's step is in flight. Locks are per conversation, but this is the main thing to watch on the dev stack.
  • isolation from the tool is ignored here: children share the parent's sandbox, and their workspace layout follows the sandbox grouping strategy.
  • No migration, config or chart change. Canvas renders the new action generically until Replace browser-executed child launch with same-backend server-side tool OpenHands#17040.

Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-8d88860

…oud parents

A Cloud parent has no way to start a child conversation today. Agent Canvas
registers its browser-executed child launch tool only for parents running on
a local agent server, and AppConversationStartRequest cannot carry client
tools.

Add POST /api/v1/webhooks/conversations/{conversation_id}/children as the
Cloud launcher for the SDK's start_child_conversation tool. It authenticates
with the sandbox session key like the other webhook callbacks, checks that
the parent belongs to the calling sandbox, and starts the child through
start_app_conversation as the parent's owner, scoped to the parent's
organization. parent_conversation_id is set on the start request, so the
child inherits the parent's sandbox, repository, branch and model, and the
usual quota and secrets handling applies. The response carries the child's
id, launch status and URL; a failed start comes back as a 500 with the start
task detail.

Attach start_child_conversation to non-planner OpenHands agents whenever
web_url is configured, with launch_url pointing at that endpoint, so the tool
runs inside the sandbox and calls back into the app server instead of
depending on a browser.

This needs an openhands-sdk release that ships start_child_conversation; the
dependency pins are bumped once that release exists.

@tofarr tofarr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🍰

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support server-side same-backend child conversation launches in Cloud

2 participants