diff --git a/migrations/versions/015_add_run_task_outcome.py b/migrations/versions/015_add_run_task_outcome.py new file mode 100644 index 0000000..2a5c104 --- /dev/null +++ b/migrations/versions/015_add_run_task_outcome.py @@ -0,0 +1,25 @@ +"""Add task_outcome column to automation_runs table. + +Revision ID: 015 +Revises: 014 +Create Date: 2026-08-12 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + + +revision: str = "015" +down_revision: str = "014" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column("automation_runs", sa.Column("task_outcome", sa.JSON, nullable=True)) + + +def downgrade() -> None: + op.drop_column("automation_runs", "task_outcome") diff --git a/openhands/automation/dispatcher.py b/openhands/automation/dispatcher.py index d32c581..d7d347b 100644 --- a/openhands/automation/dispatcher.py +++ b/openhands/automation/dispatcher.py @@ -244,6 +244,8 @@ async def _fail(error: str, disable: bool = False) -> None: env_vars = backend.build_env_vars() env_vars["AUTOMATION_CALLBACK_URL"] = callback_url env_vars["AUTOMATION_RUN_ID"] = run_id + env_vars["AUTOMATION_CALLBACK_SUPPORTS_AGENT_OUTCOME"] = "true" + env_vars["AUTOMATION_USER_ID"] = str(automation.user_id) env_vars["AUTOMATION_ORG_ID"] = str(automation.org_id) env_vars["AUTOMATION_API_URL"] = settings.resolved_base_url diff --git a/openhands/automation/execution.py b/openhands/automation/execution.py index 2c8e6d2..2057f5c 100644 --- a/openhands/automation/execution.py +++ b/openhands/automation/execution.py @@ -519,6 +519,9 @@ async def run_automation( env_vars["AUTOMATION_CALLBACK_URL"] = callback_url if run_id: env_vars["AUTOMATION_RUN_ID"] = run_id + if callback_url: + env_vars["AUTOMATION_CALLBACK_SUPPORTS_AGENT_OUTCOME"] = "true" + api_url = api_url.rstrip("/") sandbox_id: str | None = None diff --git a/openhands/automation/models.py b/openhands/automation/models.py index 7076d68..a22fb53 100644 --- a/openhands/automation/models.py +++ b/openhands/automation/models.py @@ -164,6 +164,11 @@ class AutomationRun(Base): # force-terminated by the watchdog / cancelled so no callback ever fired. cost: Mapped[float | None] = mapped_column(Float, nullable=True) + # Latest structured task outcome reported by the SDK/agent or recovered + # from the agent-server when the completion callback is missed. + # Uses generic JSON type for cross-database compatibility. + task_outcome: Mapped[dict | None] = mapped_column(JSON, nullable=True) + # Pre-computed deadline: started_at + max_duration. Set when transitioning # to RUNNING, used by the staleness watchdog for efficient indexed queries. timeout_at: Mapped[datetime | None] = mapped_column( diff --git a/openhands/automation/presets/plugin/sdk_main.py b/openhands/automation/presets/plugin/sdk_main.py index 2325e27..efaabc7 100644 --- a/openhands/automation/presets/plugin/sdk_main.py +++ b/openhands/automation/presets/plugin/sdk_main.py @@ -86,9 +86,8 @@ # may be missing here even when the agent-server has a valid session key. # We still fall back to SESSION_API_KEY for compatibility with cloud-mode # deployments and older agent-server versions that only set the bare name. -session_key = ( - os.environ.get("OH_SESSION_API_KEYS_0") - or os.environ.get("SESSION_API_KEY", "") +session_key = os.environ.get("OH_SESSION_API_KEYS_0") or os.environ.get( + "SESSION_API_KEY", "" ) model_profile = os.environ.get("AUTOMATION_MODEL") or None automation_user_id = os.environ.get("AUTOMATION_USER_ID") or None @@ -100,10 +99,7 @@ if IS_LOCAL_MODE: # Local mode: AGENT_SERVER_URL required print(f" AGENT_SERVER_URL: {'OK' if agent_server_url else 'MISSING'}") - print( - f" OH_SESSION_API_KEYS_0: " - f"{'OK' if session_key else 'NONE (may fail auth)'}" - ) + print(f" OH_SESSION_API_KEYS_0: {'OK' if session_key else 'NONE (may fail auth)'}") if not agent_server_url: print("FAIL: AGENT_SERVER_URL not set for local mode", file=sys.stderr) sys.exit(1) @@ -449,6 +445,7 @@ def event_callback(event) -> None: conversation = Conversation(**conversation_kwargs) assert isinstance(conversation, RemoteConversation) print(f" conversation created: {type(conversation).__name__}") + print(f" conversation id: {conversation.id}") print(f" plugins loaded: {len(plugin_sources)}") if experiment_tags: print(f" experiment tags: {experiment_tags}") diff --git a/openhands/automation/presets/prompt/sdk_main.py b/openhands/automation/presets/prompt/sdk_main.py index 1927c83..9a46967 100644 --- a/openhands/automation/presets/prompt/sdk_main.py +++ b/openhands/automation/presets/prompt/sdk_main.py @@ -89,9 +89,8 @@ # may be missing here even when the agent-server has a valid session key. # We still fall back to SESSION_API_KEY for compatibility with cloud-mode # deployments and older agent-server versions that only set the bare name. -session_key = ( - os.environ.get("OH_SESSION_API_KEYS_0") - or os.environ.get("SESSION_API_KEY", "") +session_key = os.environ.get("OH_SESSION_API_KEYS_0") or os.environ.get( + "SESSION_API_KEY", "" ) model_profile = os.environ.get("AUTOMATION_MODEL") or None automation_user_id = os.environ.get("AUTOMATION_USER_ID") or None @@ -103,10 +102,7 @@ if IS_LOCAL_MODE: # Local mode: AGENT_SERVER_URL required print(f" AGENT_SERVER_URL: {'OK' if agent_server_url else 'MISSING'}") - print( - f" OH_SESSION_API_KEYS_0: " - f"{'OK' if session_key else 'NONE (may fail auth)'}" - ) + print(f" OH_SESSION_API_KEYS_0: {'OK' if session_key else 'NONE (may fail auth)'}") if not agent_server_url: print("FAIL: AGENT_SERVER_URL not set for local mode", file=sys.stderr) sys.exit(1) @@ -401,6 +397,7 @@ def event_callback(event) -> None: conversation = Conversation(**conversation_kwargs) assert isinstance(conversation, RemoteConversation) print(f" conversation created: {type(conversation).__name__}") + print(f" conversation id: {conversation.id}") # Set a descriptive title so automation runs are distinguishable in the # conversation panel — otherwise the agent-server's autotitle falls back diff --git a/openhands/automation/router.py b/openhands/automation/router.py index ca11256..e2411d8 100644 --- a/openhands/automation/router.py +++ b/openhands/automation/router.py @@ -424,6 +424,12 @@ async def complete_run( values["conversation_id"] = body.conversation_id if body.cost is not None: values["cost"] = body.cost + task_outcome = ( + body.agent_outcome if body.agent_outcome is not None else body.task_outcome + ) + if task_outcome is not None: + values["task_outcome"] = task_outcome + if body.status == "FAILED" and body.error: values["error_detail"] = body.error diff --git a/openhands/automation/schemas.py b/openhands/automation/schemas.py index c7ef1c5..250ec7f 100644 --- a/openhands/automation/schemas.py +++ b/openhands/automation/schemas.py @@ -653,6 +653,14 @@ class RunCompleteRequest(BaseModel): conversation_id: str | None = None error: str | None = None cost: float | None = None + agent_outcome: dict[str, Any] | None = Field( + default=None, + description="Latest SDK task outcome reported in the completion callback.", + ) + task_outcome: dict[str, Any] | None = Field( + default=None, + description="Alias accepted for clients that send task_outcome directly.", + ) class AutomationRunResponse(BaseModel): @@ -664,6 +672,8 @@ class AutomationRunResponse(BaseModel): error_detail: str | None conversation_id: str | None cost: float | None = None + task_outcome: dict[str, Any] | None = None + timeout_at: UtcDatetime | None sandbox_id: str | None bash_command_id: str | None = None diff --git a/openhands/automation/utils/agent_server.py b/openhands/automation/utils/agent_server.py index f3fa293..f35c3ef 100644 --- a/openhands/automation/utils/agent_server.py +++ b/openhands/automation/utils/agent_server.py @@ -6,6 +6,8 @@ """ import logging +import re +from typing import Any import httpx from pydantic.dataclasses import dataclass @@ -15,6 +17,49 @@ logger = logging.getLogger(__name__) +_UUID_RE = ( + r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" +) +_CONVERSATION_URL_RE = re.compile(rf"/conversations/(?P{_UUID_RE})") +_CONVERSATION_ID_RE = re.compile( + rf"conversation(?:\s+id)?\s*:\s*(?P{_UUID_RE})", + re.IGNORECASE, +) + + +def extract_conversation_id(output: str) -> str | None: + """Extract a conversation id printed by automation preset scripts.""" + for pattern in (_CONVERSATION_ID_RE, _CONVERSATION_URL_RE): + match = pattern.search(output) + if match: + return match.group("id") + return None + + +async def get_conversation_task_outcome( + client: httpx.AsyncClient, + agent_url: str, + session_key: str, + conversation_id: str, +) -> dict[str, Any] | None: + """Fetch the latest task_outcome from an agent-server conversation.""" + try: + resp = await client.get( + f"{agent_url.rstrip('/')}/api/conversations/{conversation_id}", + headers={"X-Session-API-Key": session_key}, + timeout=30.0, + ) + resp.raise_for_status() + outcome = resp.json().get("task_outcome") + return outcome if isinstance(outcome, dict) else None + except Exception as e: + logger.warning( + "Failed to fetch task outcome for conversation %s: %s", + conversation_id, + e, + ) + return None + @dataclass(frozen=True) class BashCommandResult: @@ -112,6 +157,8 @@ class VerificationResult: stdout: str = "" stderr: str = "" error: str | None = None + conversation_id: str | None = None + task_outcome: dict[str, Any] | None = None async def verify_run_on_agent_server( @@ -168,6 +215,12 @@ async def verify_run_on_agent_server( ) success = bash_result.exit_code == 0 + conversation_id = extract_conversation_id(bash_result.stdout) + task_outcome = None + if conversation_id: + task_outcome = await get_conversation_task_outcome( + client, agent_url, session_key, conversation_id + ) logger.info( "Verified run status: exit_code=%s, success=%s", bash_result.exit_code, @@ -181,4 +234,6 @@ async def verify_run_on_agent_server( exit_code=bash_result.exit_code, stdout=bash_result.stdout, stderr=bash_result.stderr, + conversation_id=conversation_id, + task_outcome=task_outcome, ) diff --git a/openhands/automation/utils/sandbox.py b/openhands/automation/utils/sandbox.py index fbe3590..cabe517 100644 --- a/openhands/automation/utils/sandbox.py +++ b/openhands/automation/utils/sandbox.py @@ -14,6 +14,8 @@ from openhands.automation.utils.agent_server import ( BashCommandResult, VerificationResult, + extract_conversation_id, + get_conversation_task_outcome, get_last_bash_command_result, ) from openhands.automation.utils.log_context import log_extra @@ -195,6 +197,12 @@ async def verify_run_status( ) success = bash_result.exit_code == 0 + conversation_id = extract_conversation_id(bash_result.stdout) + task_outcome = None + if conversation_id: + task_outcome = await get_conversation_task_outcome( + client, agent_url, session_key, conversation_id + ) logger.info( "Verified run status: exit_code=%s, success=%s", bash_result.exit_code, @@ -208,4 +216,6 @@ async def verify_run_status( exit_code=bash_result.exit_code, stdout=bash_result.stdout, stderr=bash_result.stderr, + conversation_id=conversation_id, + task_outcome=task_outcome, ) diff --git a/openhands/automation/watchdog.py b/openhands/automation/watchdog.py index 6235704..fae82b9 100644 --- a/openhands/automation/watchdog.py +++ b/openhands/automation/watchdog.py @@ -13,6 +13,7 @@ import asyncio import logging +from typing import Any from sqlalchemy import inspect, select, update from sqlalchemy.engine import CursorResult @@ -117,6 +118,12 @@ async def _verify_and_mark_run( return result.rowcount > 0 if verification.verified: + verification_values: dict[str, Any] = {} + if verification.conversation_id: + verification_values["conversation_id"] = verification.conversation_id + if verification.task_outcome is not None: + verification_values["task_outcome"] = verification.task_outcome + exit_code = verification.exit_code # exit_code == 0: Command completed successfully, we just missed the callback @@ -136,6 +143,7 @@ async def _verify_and_mark_run( .values( status=AutomationRunStatus.COMPLETED, completed_at=now, + **verification_values, ) ) @@ -160,6 +168,7 @@ async def _verify_and_mark_run( status=AutomationRunStatus.FAILED, completed_at=now, error_detail=f"Timed out: {error_msg}", + **verification_values, ) ) @@ -187,6 +196,7 @@ async def _verify_and_mark_run( status=AutomationRunStatus.FAILED, completed_at=now, error_detail=error_detail, + **verification_values, ) ) diff --git a/pyproject.toml b/pyproject.toml index fe0127a..d95cf24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,8 @@ dependencies = [ "google-cloud-storage>=2.18", "httpx>=0.27", "jmespath>=1.0", - "openhands-sdk==1.40.1", - "openhands-workspace==1.40.1", + "openhands-sdk @ git+https://github.com/OpenHands/software-agent-sdk.git@be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3#subdirectory=openhands-sdk", + "openhands-workspace @ git+https://github.com/OpenHands/software-agent-sdk.git@be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3#subdirectory=openhands-workspace", "pg8000>=1.31", "prometheus-client>=0.19", "pydantic>=2", diff --git a/tests/test_db.py b/tests/test_db.py index 8df9f9c..e2f2ede 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -260,6 +260,8 @@ def test_migrations_run_on_sqlite(self, monkeypatch): } assert "cost" in run_columns + assert "task_outcome" in run_columns + engine.dispose() finally: # Clean up diff --git a/tests/test_local_mode.py b/tests/test_local_mode.py index 6704696..5eebee7 100644 --- a/tests/test_local_mode.py +++ b/tests/test_local_mode.py @@ -5,6 +5,8 @@ from openhands.automation.utils.agent_server import ( BashCommandResult, VerificationResult, + extract_conversation_id, + get_conversation_task_outcome, get_last_bash_command_result, verify_run_on_agent_server, ) @@ -53,6 +55,49 @@ def test_completed_failure(self): assert result.stderr == "Error: file not found" +class TestTaskOutcomeRecoveryHelpers: + """Tests for recovering task outcomes from agent-server conversations.""" + + def test_extract_conversation_id_from_printed_id(self): + conversation_id = "12345678-1234-5678-1234-567812345678" + output = f" conversation id: {conversation_id}\nALL_OK" + + assert extract_conversation_id(output) == conversation_id + + def test_extract_conversation_id_from_session_url(self): + conversation_id = "12345678-1234-5678-1234-567812345678" + output = ( + f"session URL: https://app.all-hands.dev/conversations/{conversation_id}" + ) + + assert extract_conversation_id(output) == conversation_id + + @pytest.mark.asyncio + async def test_get_conversation_task_outcome(self): + from unittest.mock import AsyncMock, MagicMock + + import httpx + + outcome = {"status": "blocked", "summary": "Missing Slack MCP"} + mock_client = MagicMock(spec=httpx.AsyncClient) + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = {"task_outcome": outcome} + mock_client.get = AsyncMock(return_value=mock_response) + + result = await get_conversation_task_outcome( + mock_client, + "http://localhost:3000", + "test-key", + "12345678-1234-5678-1234-567812345678", + ) + + assert result == outcome + mock_client.get.assert_awaited_once() + _, kwargs = mock_client.get.call_args + assert kwargs["headers"] == {"X-Session-API-Key": "test-key"} + + class TestVerificationResult: """Tests for VerificationResult dataclass.""" @@ -303,6 +348,41 @@ async def test_returns_verified_success(self): assert result.exit_code == 0 assert result.stdout == "Done" + @pytest.mark.asyncio + async def test_returns_recovered_task_outcome(self): + """Verified runs include task outcome fetched from conversation info.""" + from unittest.mock import patch + + conversation_id = "12345678-1234-5678-1234-567812345678" + outcome = {"status": "success", "summary": "Done"} + mock_result = BashCommandResult( + found=True, + exit_code=0, + stdout=f"conversation id: {conversation_id}\nDone", + stderr="", + ) + + with ( + patch( + "openhands.automation.utils.agent_server.get_last_bash_command_result" + ) as mock_get, + patch( + "openhands.automation.utils.agent_server.get_conversation_task_outcome" + ) as mock_outcome, + ): + mock_get.return_value = mock_result + mock_outcome.return_value = outcome + + result = await verify_run_on_agent_server( + agent_url="http://localhost:3000", + session_key="test-key", + run_id="run-123", + ) + + assert result.verified is True + assert result.conversation_id == conversation_id + assert result.task_outcome == outcome + @pytest.mark.asyncio async def test_returns_verified_failure(self): """Returns verified failure when exit_code is non-zero.""" diff --git a/tests/test_router.py b/tests/test_router.py index 6491eb4..c39d8a0 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -2055,6 +2055,31 @@ async def test_complete_run_without_cost_leaves_it_unset( await async_session.refresh(run) assert run.cost is None + @pytest.mark.parametrize("field_name", ["agent_outcome", "task_outcome"]) + async def test_complete_run_saves_task_outcome( + self, async_client, async_session, field_name + ): + """Complete endpoint stores the latest SDK task outcome.""" + run = await self._running_run(async_session) + outcome = { + "status": "blocked", + "summary": "Slack MCP is not configured.", + "blockers": ["Missing Slack MCP configuration"], + "confidence": 0.94, + "needs_user_action": True, + "source": "agent", + } + + response = await async_client.post( + f"/api/automation/v1/runs/{run.id}/complete", + json={"status": "COMPLETED", field_name: outcome}, + ) + + assert response.status_code == 200 + assert response.json()["task_outcome"] == outcome + await async_session.refresh(run) + assert run.task_outcome == outcome + async def test_first_completed_run_records_success_outcome( self, async_client, async_session ): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index cbe7407..6e42d49 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -110,6 +110,12 @@ def test_none_optional_fields_remain_none(self): assert data["completed_at"] is None assert data["timeout_at"] is None + def test_task_outcome_serialises_as_json_object(self): + outcome = {"status": "blocked", "summary": "Missing Slack credentials"} + run = self._make_run(task_outcome=outcome) + data = run.model_dump(mode="json") + assert data["task_outcome"] == outcome + def test_already_utc_aware_datetime_serialises_correctly(self): run = self._make_run(created_at=_UTC_AWARE) data = run.model_dump(mode="json") diff --git a/tests/test_watchdog.py b/tests/test_watchdog.py index ddaca86..0de7af3 100644 --- a/tests/test_watchdog.py +++ b/tests/test_watchdog.py @@ -103,6 +103,47 @@ async def test_exit_code_0_marks_completed( assert run.error_detail is None @pytest.mark.asyncio + async def test_exit_code_0_persists_recovered_task_outcome( + self, async_session_factory, automation_with_run, mock_settings + ): + """Missed callbacks still persist task outcome recovered from agent-server.""" + run_id = automation_with_run["run_id"] + outcome = { + "status": "success", + "summary": "Task completed.", + "blockers": [], + "confidence": 0.98, + "needs_user_action": False, + "source": "agent", + } + conversation_id = "12345678-1234-5678-1234-567812345678" + + verification = VerificationResult( + verified=True, + success=True, + exit_code=0, + stdout="Success output", + stderr="", + conversation_id=conversation_id, + task_outcome=outcome, + ) + + mock_backend = _create_mock_backend(verification) + with patch( + "openhands.automation.watchdog.get_backend", return_value=mock_backend + ): + async with async_session_factory() as session: + run = await session.get(AutomationRun, run_id) + result = await _verify_and_mark_run(session, run, mock_settings) + await session.commit() + + assert result is True + async with async_session_factory() as session: + run = await session.get(AutomationRun, run_id) + assert run.status == AutomationRunStatus.COMPLETED + assert run.conversation_id == conversation_id + assert run.task_outcome == outcome + async def test_exit_code_0_keep_alive_true_skips_cleanup( self, async_session_factory, automation_with_run, mock_settings ): diff --git a/uv.lock b/uv.lock index cf50b13..64033d0 100644 --- a/uv.lock +++ b/uv.lock @@ -2157,7 +2157,7 @@ wheels = [ [[package]] name = "openai" -version = "2.24.0" +version = "2.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2169,9 +2169,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/9a/8c75e8c8a5b407a0586faeb2afac91674ff955c191ecc1d6d3b6669f6788/openai-2.54.0.tar.gz", hash = "sha256:e3e6f8bc1ba30ddf381ace1a14340eed381cb984a1a59bd0f34b5be3b5d49cfa", size = 1100285, upload-time = "2026-08-11T18:46:59.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/64/a8/bb76c7356de8ad57f59d5ff993d434df0607f07f08bcc9c9a5c275e399c0/openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b", size = 1660351, upload-time = "2026-08-11T18:46:56.684Z" }, ] [[package]] @@ -2188,13 +2188,14 @@ wheels = [ [[package]] name = "openhands-agent-server" -version = "1.19.1" -source = { registry = "https://pypi.org/simple" } +version = "1.41.0" +source = { git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-agent-server&rev=be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3#be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3" } dependencies = [ { name = "aiosqlite" }, { name = "alembic" }, { name = "docker" }, { name = "fastapi" }, + { name = "openai" }, { name = "openhands-sdk" }, { name = "pydantic" }, { name = "sqlalchemy" }, @@ -2202,10 +2203,6 @@ dependencies = [ { name = "websockets" }, { name = "wsproto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/11/351784b7c6e92583f9832081e9fd7f82984cc9f3e51bb62511df3a138119/openhands_agent_server-1.19.1.tar.gz", hash = "sha256:0cb8d6a681b0645e1e2391e81466436b5573a56f8985e76620902dab2bcc5092", size = 88827, upload-time = "2026-04-30T17:06:46.015Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/5a/d5080844f3bd97c51ad1e24db78520aff6f97458035989a3f16a57503b7d/openhands_agent_server-1.19.1-py3-none-any.whl", hash = "sha256:4c685bcf7f941f087878f51dca2071a110fea360798da4020fc89b034bee7fda", size = 105530, upload-time = "2026-04-30T17:06:48.528Z" }, -] [[package]] name = "openhands-automation" @@ -2264,8 +2261,8 @@ requires-dist = [ { name = "google-cloud-storage", specifier = ">=2.18" }, { name = "httpx", specifier = ">=0.27" }, { name = "jmespath", specifier = ">=1.0" }, - { name = "openhands-sdk", specifier = "==1.40.1" }, - { name = "openhands-workspace", specifier = "==1.40.1" }, + { name = "openhands-sdk", git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-sdk&rev=be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3" }, + { name = "openhands-workspace", git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-workspace&rev=be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3" }, { name = "pg8000", specifier = ">=1.31" }, { name = "prometheus-client", specifier = ">=0.19" }, { name = "pydantic", specifier = ">=2" }, @@ -2294,8 +2291,8 @@ dev = [ [[package]] name = "openhands-sdk" -version = "1.40.1" -source = { registry = "https://pypi.org/simple" } +version = "1.41.0" +source = { git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-sdk&rev=be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3#be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3" } dependencies = [ { name = "agent-client-protocol" }, { name = "deprecation" }, @@ -2304,6 +2301,7 @@ dependencies = [ { name = "filelock" }, { name = "httpx", extra = ["socks"] }, { name = "joserfc" }, + { name = "jsonschema" }, { name = "litellm" }, { name = "lmnr" }, { name = "pillow" }, @@ -2315,24 +2313,16 @@ dependencies = [ { name = "tree-sitter-bash" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/9c/f3d9a7defbc8dd2d834904d46a706229f56dbc751e234343fca81ccc6bb8/openhands_sdk-1.40.1.tar.gz", hash = "sha256:14205c416f327d365826d9e1f14f7514624887269b0ebeec541555f29b63a2cd", size = 655351, upload-time = "2026-08-05T13:04:25.827Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/6f/827a7cb418b6d4e413aef58029f7ed0bb39939da5ef5f1b4103a3e4bf5f8/openhands_sdk-1.40.1-py3-none-any.whl", hash = "sha256:8d574a4104b01bb24d489a7fdf6ed7edf04c301500aae9cc6ae20290524ad9b0", size = 782746, upload-time = "2026-08-05T13:04:20.747Z" }, -] [[package]] name = "openhands-workspace" -version = "1.40.1" -source = { registry = "https://pypi.org/simple" } +version = "1.41.0" +source = { git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-workspace&rev=be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3#be17960b7846fdfc4bd66ce2fb6dd40bd890f6b3" } dependencies = [ { name = "openhands-agent-server" }, { name = "openhands-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/ad/e4dac9640ef3a9d8be5c9beebdf4f01df32a803139346e3502533935dbbf/openhands_workspace-1.40.1.tar.gz", hash = "sha256:9eed0e039e36f1740918193d9f20713de686c54001018accc6575ca591d9bfc9", size = 23988, upload-time = "2026-08-05T13:04:27.778Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/05/fcc5303c65af3f1b89bc056a38698e08c091c568e6fa1124ed11b1975c98/openhands_workspace-1.40.1-py3-none-any.whl", hash = "sha256:a48e2018a5a9f0b9623be546d1a9f7954f2cc3cc92afac291951f117dcf80ed8", size = 28317, upload-time = "2026-08-05T13:04:23.473Z" }, -] [[package]] name = "opentelemetry-api"