Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .pr/orchestration-agent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions .pr/orchestration-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Orchestration guide evidence

September 13, 2026. Guide source: `5b5bf8a8`.

The actual Mintlify page rendered with HTTP 200 and no browser errors. Both Python blocks were extracted unchanged from the guide and executed against an isolated Docker Agent Server containing the unreleased SDK client. The first uploaded a harmless archive and created `Scheduled automation`. A preceding user message configured the workflow to reply `DOCS_ORCHESTRATION_PASS` without tools, GitHub access, secrets, or file changes; it used `run=False`. The second block attached, ran the model, and printed that response. Canvas displayed the resulting conversation and response.

![Rendered guide](https://raw.githubusercontent.com/OpenHands/docs/4b0e728244ae9cbcdd52e8b3b61dfe2cc22511b3/.pr/orchestration-guide.png)
![Documented example's actual Canvas response](https://raw.githubusercontent.com/OpenHands/docs/4b0e728244ae9cbcdd52e8b3b61dfe2cc22511b3/.pr/orchestration-agent.png)

[Credential-free result](https://github.com/OpenHands/docs/blob/4b0e728244ae9cbcdd52e8b3b61dfe2cc22511b3/.pr/orchestration-live.json). The disposable runtime was released after capture. No application repository was modified. The example calls `register_default_tools()` before attachment; this run verifies a simple model response, not every registered tool. The source-built SDK satisfies unreleased prerequisites; this does not claim the current PyPI package contains these APIs.

Reproduce by extracting the guide's Python blocks, supplying an isolated server URL/key and saved profile, calling `dispatch()` with a fixture archive, and executing the attachment block with the returned conversation ID/workspace. Capture the Canvas response before releasing its runtime.
Binary file added .pr/orchestration-guide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions .pr/orchestration-live.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"conversation_id": "c2f311e4-e42b-4adf-a793-3a9c3e5efa09",
"canvas_url": "http://127.0.0.1:9101/conversations/c2f311e4-e42b-4adf-a793-3a9c3e5efa09",
"source": "docs793 exact two Python blocks; configured benign workflow via preceding user message",
"working_dir": "/workspace",
"title": "Scheduled automation",
"dispatch_upload_pass": true,
"attachment_real_model_turn_pass": true,
"expected_response": "DOCS_ORCHESTRATION_PASS",
"registered_default_tools": true,
"application_repository_access": false,
"canvas_response_visible": true
}
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@
"group": "Remote Agent Server",
"pages": [
"sdk/guides/agent-server/overview",
"sdk/guides/agent-server/orchestration-clients",
"sdk/guides/agent-server/local-server",
"sdk/guides/agent-server/conversation-goals",
"sdk/guides/agent-server/docker-sandbox",
Expand Down Expand Up @@ -853,4 +854,4 @@
"destination": "/openhands/usage/automations/overview"
}
]
}
}
189 changes: 189 additions & 0 deletions sdk/guides/agent-server/orchestration-clients.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
title: Orchestrate Agent Server Conversations
description: Create conversations from agent profiles and control their workspaces with the existing SDK APIs.
---

<Note>
This guide accompanies [software-agent-sdk #5010](https://github.com/OpenHands/software-agent-sdk/pull/5010).
Use a build containing that change and the server's conversation-scoped runtime APIs.
Publish this guide with the corresponding SDK release.
</Note>

Use `RemoteConversation.create` to create an agent conversation from a saved
profile and `RemoteConversation.attach` to connect to an existing conversation.
Use `RemoteWorkspace` for its files, commands, and runtime lifecycle.
`AsyncRemoteWorkspace` provides the same workspace operations for async callers.

The SDK owns HTTP routes, authentication headers, and conversation scope. The
caller owns scheduling, admission limits, workflow selection, and completion
policy. Canvas uses the TypeScript client for the same API boundary.

## Create a Conversation From a Saved Profile

A dispatcher selects a unique conversation ID and workspace directory. Use
`/workspace` for a Docker conversation, or an absolute local run directory for a
local server. `workspace.get_server_info()["conversation_runtime"]` reports the
server's configured runtime mode. This selection belongs to the dispatcher;
the automation bundle receives the same environment in either mode.

```python
import os
from contextlib import closing
from uuid import UUID, uuid4

from openhands.sdk import RemoteConversation, RemoteWorkspace
from openhands.sdk.conversation.request import StartConversationRequest
from openhands.sdk.workspace import LocalWorkspace
from openhands.tools import register_default_tools

register_default_tools()
conversation_id = uuid4()
working_dir = os.environ["WORKSPACE_BASE"]

with RemoteWorkspace(
host=os.environ["AGENT_SERVER_URL"],
api_key=os.environ["SESSION_API_KEY"],
working_dir=working_dir,
runtime_conversation_id=conversation_id,
) as workspace:
with closing(RemoteConversation.create(
workspace=workspace,
request=StartConversationRequest(
agent_profile_id=UUID(os.environ["AGENT_PROFILE_ID"]),
workspace=LocalWorkspace(working_dir=working_dir),
conversation_id=conversation_id,
tags={"automationrun": str(conversation_id)},
),
visualizer=None,
)) as conversation:
conversation.set_title("Scheduled automation")
workspace.file_upload(b"Prepared by the dispatcher\n", f"{working_dir}/input.txt")
```

`create` sends a typed creation request directly to the server. The server
resolves the saved profile. The dispatcher does not load its model,
tools, or secret values. `runtime_conversation_id` binds the workspace's existing

Check warning on line 65 in sdk/guides/agent-server/orchestration-clients.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/guides/agent-server/orchestration-clients.mdx#L65

Did you really mean 'workspace's'?
file, command, and Git operations to that conversation and cannot be changed
on the workspace instance. Omitting it selects the legacy host workspace;
runtime credential handoff and release require an explicit conversation scope.

## Start and Observe a Bundle

After uploading and extracting its bundle, a dispatcher starts the configured
entrypoint with `workspace.start_command(command, timeout=...)`. This returns a
command ID without waiting. `workspace.get_command_output(command_id)` returns
the latest matching output record; `None` or a missing exit code means the
command has not completed. Always inspect the actual command's exit code before
accepting completion. `workspace.execute_command(...)` remains the foreground
operation and returns a `CommandResult`.

Async callers use the same scope and operations:

```python
import os

from openhands.sdk.workspace import AsyncRemoteWorkspace


async def start_worker(conversation_id, working_dir, command):
async with AsyncRemoteWorkspace(
host=os.environ["AGENT_SERVER_URL"],
api_key=os.environ["SESSION_API_KEY"],
working_dir=working_dir,
runtime_conversation_id=conversation_id,
) as workspace:
return await workspace.start_command(command, timeout=2400)
```

## The Bundle Interface

Supply `AGENT_SERVER_URL`, `SESSION_API_KEY`, `AUTOMATION_CONVERSATION_ID`, and
`WORKSPACE_BASE` to each bundle. The same code works in either workspace:

```python
import os
from contextlib import closing
from uuid import UUID

from openhands.tools import register_default_tools
from openhands.sdk import RemoteConversation, RemoteWorkspace
from openhands.sdk.conversation.response_utils import get_agent_final_response

register_default_tools()

with RemoteWorkspace(
host=os.environ["AGENT_SERVER_URL"],
api_key=os.environ["SESSION_API_KEY"],
working_dir=os.environ["WORKSPACE_BASE"],
) as workspace:
with closing(RemoteConversation.attach(
workspace=workspace,
conversation_id=UUID(os.environ["AUTOMATION_CONVERSATION_ID"]),
visualizer=None,
)) as conversation:
conversation.send_message("Perform the configured workflow.")
conversation.run(timeout=2400)
print(get_agent_final_response(conversation.state.events))
```

`attach` fetches an existing conversation and connects to its server-resolved
agent, including ACP agents. A missing or inaccessible conversation fails;
attachment never creates a conversation or reloads its profile. Run completion,
callbacks, structured events, and errors follow the existing conversation lifecycle. Read `conversation.state.events`
for the event history.

`delete_on_close` defaults to `False`, so closing this handle preserves the
server conversation and the caller-owned workspace. Register the remote agent's
tool schemas before connecting: `register_default_tools()` covers standard
tools, including browser events; register additional custom tools separately.

## Select Secrets With an Agent Profile

Profile secret selection requires the profile `secret_refs` and Docker integration
changes tracked in [SDK #5014](https://github.com/OpenHands/software-agent-sdk/issues/5014).
Keep this section unpublished until that integration is released.

Servers with profile-secret enforcement advertise `profile_secret_scope_v1` in
`/server_info.capabilities`; clients should use that capability instead of a guessed
release version when offering a scope picker.

Set `secret_refs` on the saved agent profile to an explicit list of secret names,
for example `["GITHUB_REVIEWER_TOKEN"]`. The profile stores names only. A launch
using that profile receives the selected saved secrets without the dispatcher
copying every secret into the request. An empty list permits no user secrets;
`null` preserves the existing request-only, unrestricted behavior.

The Agent Server applies the same selection before local conversation construction
and before Docker secret materialization. Extra request secrets are discarded
before their lookup runs. For a selected name present in the server's secret store,
the stored value takes precedence over a caller-supplied value or lookup. Names
missing from the store can still match explicitly supplied conversation secrets.
The agent's secret registry advertises selected names and descriptions in the
system prompt and exports selected values to tools when needed. Values are never
included in the prompt. Docker handoff encrypts them with the runtime's key.

LLM profile credentials and selected MCP credentials have their own delivery
channels. Restrict `mcp_server_refs`, tools, and other profile capabilities as well.
A secret allow-list does not narrow the external provider's token permissions:
issue repository-scoped credentials appropriate to each automation's role.

## Credentials and Lifecycle Ownership

Only a trusted orchestrator should call `workspace.get_runtime_session_key()`. On a Docker
server supporting credential handoff, it returns the selected inner runtime's key.
The dispatcher supplies that key and the runtime-reachable server URL to the
worker. Keep the outer control-plane key outside the worker.

Local workspaces retain the local server's credential boundary and are not
security sandboxes. Use Docker isolation and role-scoped external-service grants
when running untrusted repository code.

Check warning on line 179 in sdk/guides/agent-server/orchestration-clients.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/guides/agent-server/orchestration-clients.mdx#L179

Did you really mean 'untrusted'?

After a Docker run has stopped and its result is recorded, the dispatcher calls
`workspace.release_runtime()`. This releases that runtime while preserving conversation
history. A missing runtime is treated as already released; other failures are
reported. A persistent local server should be retained by its owner.

Workspace context managers close their own HTTP connections. Closing a
conversation handle does not release its runtime or delete history unless
`delete_on_close=True` was explicitly selected. These SDK operations do not
choose a workflow, post an acceptance decision, or schedule another run.
2 changes: 2 additions & 0 deletions sdk/guides/agent-server/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
description: Run agents on remote servers with isolated workspaces for production deployments.
---

Remote Agent Servers package the Software Agent SDK into containers you can deploy anywhere (Kubernetes, VMs, on‑prem, any cloud) with strong isolation. The remote path uses the exact same SDK API as local—switching is just changing the workspace argument; your Conversation code stays the same.

Check warning on line 6 in sdk/guides/agent-server/overview.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/guides/agent-server/overview.mdx#L6

Did you really mean 'VMs'?


For example, switching from a local workspace to a Docker‑based remote agent server:
Expand Down Expand Up @@ -35,10 +35,12 @@
```


For dispatcher and automation integrations, see [Orchestrate Agent Server Conversations](/sdk/guides/agent-server/orchestration-clients).

## What is a Remote Agent Server?

A Remote Agent Server is an HTTP/WebSocket server that:
- **Package the Software Agent SDK into containers** and deploy on your own infrastructure (Kubernetes, VMs, on-prem, or cloud)

Check warning on line 43 in sdk/guides/agent-server/overview.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/guides/agent-server/overview.mdx#L43

Did you really mean 'VMs'?
- **Runs agents** on dedicated infrastructure
- **Manages workspaces** (Docker containers or remote sandboxes)
- **Streams events** to clients via WebSocket
Expand Down Expand Up @@ -112,7 +114,7 @@
- Waits for it to be ready.
- Shares the server URL with the SDK client.

You don’t need to manage this manually—the workspace context handles startup and teardown automatically.

Check warning on line 117 in sdk/guides/agent-server/overview.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/guides/agent-server/overview.mdx#L117

Did you really mean 'teardown'?

### 3. Event Streaming → *(Bidirectional WebSocket)*

Expand Down Expand Up @@ -142,7 +144,7 @@
print(result.stdout)
```

These commands are proxied through the agent server, whether it’s a Docker container or a remote VM, keeping your client code environment-agnostic.

Check warning on line 147 in sdk/guides/agent-server/overview.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/guides/agent-server/overview.mdx#L147

Did you really mean 'proxied'?

### Summary

Expand Down
Loading