-
Notifications
You must be signed in to change notification settings - Fork 522
feat(sdk): extend existing conversation and workspace APIs for automation #5010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
5cdfb5f
feat(sdk): expose Agent Server control and runtime clients
openhands-agent 34cd7ec
refactor(sdk): initialize orchestration transports on first request
openhands-agent 79021c6
fix(sdk): reject empty explicit runtime scope
openhands-agent 3509fa7
test(sdk): keep parametrized client scopes stable across workers
openhands-agent df0f25d
Merge remote-tracking branch 'origin/main' into factory/ready-sdk-5010
openhands-agent 334cf4a
chore: Remove PR-only artifacts [automated]
dfb537c
fix: expose runtime prefix adapter on synchronous client
openhands-agent d41ab13
Merge remote-tracking branch 'origin/feat/agent-server-python-client'…
openhands-agent 85b8bc7
feat(client): preserve plugin configuration on profile launches
openhands-agent 252417d
feat: expose the existing final agent response through both clients
openhands-agent 7c0fd01
fix: set conversation titles through the existing update endpoint
openhands-agent ed0112b
Merge remote-tracking branch 'origin/main' into factory/ready-sdk-5010
openhands-agent 6573d2d
feat: attach to an existing server-resolved conversation
openhands-agent d76aa33
refactor(client): share event search and title operations
openhands-agent 34ef2b2
fix: use server event kind for client error lookup
openhands-agent fb23e8a
docs: record live Canvas failure and client error comparison
openhands-agent 780a4fb
chore: Remove PR-only artifacts [automated]
1f453fe
refactor(sdk): reuse conversations and workspaces for automation
openhands-agent 4204b7e
fix(sdk): scope downloads and Git workspace operations
openhands-agent a2c0cf2
refactor(sdk): share profile and explicit-agent creation
openhands-agent b56bd00
Merge remote-tracking branch 'origin/main' into factory/ready-sdk-5010
openhands-agent 70c3afe
refactor(sdk): make remote creation and attachment explicit
openhands-agent 4bbab2d
fix(sdk): omit null fields in conversation creation requests
openhands-agent de5bc17
refactor(sdk): use the canonical conversation route
openhands-agent e9392e7
Merge main with conversation-scoped runtime APIs
openhands-agent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Agent Server orchestration clients | ||
|
|
||
| The factory exposed a gap between the high-level Conversation/Workspace interfaces and trusted control-plane operations: saved-profile creation, selected-runtime credentials, release preserving history, and nonblocking command dispatch. Consumers currently encode these HTTP contracts themselves. | ||
|
|
||
| Add public sync/async AgentServerClient and scoped RuntimeClient interfaces in the SDK. Share operation construction in one internal module, so synchronous and asynchronous callers use identical routes, parameters, and auth. Keep scheduling, admission, retry policy, workflow prompts, and acceptance decisions in their owning consumers. Retain existing Conversation/Workspace behavior without migration. | ||
|
|
||
| Server-level metadata and conversation creation remain global. A runtime binds a validated conversation UUID and exposes named methods rather than arbitrary URL access. Explicit legacy host-runtime access supports existing dispatch consumers; lifecycle methods reject that unscoped form. A narrow API-prefix migration helper validates old caller state rather than trusting arbitrary paths. | ||
|
|
||
| Response objects retain additive server fields. The credential accessor validates a nonempty key. Release treats 404 as already released but propagates stop failures. Injected HTTP clients stay caller-owned; otherwise close/aclose releases the client pool. | ||
|
|
||
| Server contract review precedes this consumer: #4966/#3403, #4998/#5005, #5008. The client is an additive main-based PR; it changes none of those server implementations. Automation and extension consumers will use these methods and remove raw runtime HTTP code. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """Public clients for Agent Server control and conversation-scoped runtimes.""" | ||
|
|
||
| from openhands.sdk.client.agent_server import ( | ||
| AgentServerClient, | ||
| AsyncAgentServerClient, | ||
| AsyncRuntimeClient, | ||
| RuntimeClient, | ||
| ) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "AgentServerClient", | ||
| "AsyncAgentServerClient", | ||
| "AsyncRuntimeClient", | ||
| "RuntimeClient", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| """Wire contracts shared by synchronous and asynchronous Agent Server clients.""" | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from typing import Any | ||
| from uuid import UUID | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class Operation: | ||
| method: str | ||
| path: str | ||
| options: dict[str, Any] = field(default_factory=dict) | ||
| allowed_statuses: frozenset[int] = frozenset() | ||
|
|
||
|
|
||
| def conversation_path(conversation_id: str) -> str: | ||
| return f"/api/conversations/{UUID(conversation_id)}" | ||
|
|
||
|
|
||
| def create_conversation( | ||
| *, | ||
| conversation_id: str, | ||
| agent_profile_id: str, | ||
| working_dir: str, | ||
| title: str, | ||
| max_iterations: int = 160, | ||
| tags: dict[str, str] | None = None, | ||
| ) -> Operation: | ||
| return Operation( | ||
| "POST", | ||
| "/api/conversations", | ||
| { | ||
| "json": { | ||
| "conversation_id": str(UUID(conversation_id)), | ||
| "agent_profile_id": str(UUID(agent_profile_id)), | ||
| "workspace": {"kind": "LocalWorkspace", "working_dir": working_dir}, | ||
| "title": title, | ||
| "max_iterations": max_iterations, | ||
| "tags": tags or {}, | ||
| }, | ||
| "timeout": 180, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def message(conversation_id: str, text: str, run: bool) -> Operation: | ||
| return Operation( | ||
| "POST", | ||
| conversation_path(conversation_id) + "/events", | ||
| { | ||
| "json": {"content": [{"type": "text", "text": text}], "run": run}, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def errors(conversation_id: str, limit: int) -> Operation: | ||
| if not 1 <= limit <= 100: | ||
| raise ValueError("Error page limit must be between 1 and 100") | ||
| return Operation( | ||
| "GET", | ||
| conversation_path(conversation_id) + "/events/search", | ||
| { | ||
| "params": { | ||
| "kind": "ConversationErrorEvent", | ||
| "sort_order": "TIMESTAMP_DESC", | ||
| "limit": limit, | ||
| }, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| class RuntimeRequests: | ||
| """A runtime scope cannot be changed by an operation's arguments.""" | ||
|
|
||
| def __init__(self, conversation_id: str | None): | ||
| # None is the explicit legacy host runtime, not an automatic fallback. | ||
| self.api_prefix = ( | ||
| conversation_path(conversation_id) | ||
| if conversation_id is not None | ||
| else "/api" | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_api_prefix(cls, prefix: str) -> "RuntimeRequests": | ||
| """Compatibility for consumers migrating from a stored API prefix.""" | ||
| if prefix == "/api": | ||
| return cls(None) | ||
| root = "/api/conversations/" | ||
| if not prefix.startswith(root): | ||
| raise ValueError("Expected a conversation runtime scope") | ||
| return cls(prefix[len(root) :]) | ||
|
|
||
| def upload(self, path: str, content: bytes, filename: str) -> Operation: | ||
| return Operation( | ||
| "POST", | ||
| self.api_prefix + "/file/upload", | ||
| { | ||
| "params": {"path": path}, | ||
| "files": {"file": (filename, content)}, | ||
| }, | ||
| ) | ||
|
|
||
| def bash( | ||
| self, command: str, timeout: int, *, background: bool, cwd: str | None = None | ||
| ) -> Operation: | ||
| payload: dict[str, Any] = {"command": command, "timeout": timeout} | ||
| if cwd is not None: | ||
| payload["cwd"] = cwd | ||
| operation = "start_bash_command" if background else "execute_bash_command" | ||
| return Operation( | ||
| "POST", | ||
| self.api_prefix + "/bash/" + operation, | ||
| { | ||
| "json": payload, | ||
| "timeout": 90 if background else timeout + 30, | ||
| }, | ||
| ) | ||
|
|
||
| def output(self, command_id: str | None) -> Operation: | ||
| params = {"kind__eq": "BashOutput", "sort_order": "TIMESTAMP_DESC", "limit": 1} | ||
| if command_id is not None: | ||
| params["command_id__eq"] = command_id | ||
| return Operation( | ||
| "GET", self.api_prefix + "/bash/bash_events/search", {"params": params} | ||
| ) | ||
|
|
||
| def lifecycle(self, *, credentials: bool) -> Operation: | ||
| if self.api_prefix == "/api": | ||
| raise ValueError("Lifecycle operations require a conversation runtime") | ||
| return Operation( | ||
| "POST" if credentials else "DELETE", | ||
| self.api_prefix + ("/runtime/credentials" if credentials else "/runtime"), | ||
| {"timeout": 60}, | ||
| frozenset() if credentials else frozenset({404}), | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.