"""Adapter from the verified text Open Agent API to ProcessingTransport.""" from __future__ import annotations import hashlib from typing import Any, Callable, Mapping, Optional from agent_integration.client import ( OpenAgentAPIError, OpenAgentProtocolError, OpenAgentTransportError, ) from agent_integration.service import AgentResponseError, OpenAgentService from arr_processing.contracts import PROCESSING_REQUEST_VERSION, ProcessingRequest from arr_processing.errors import ProcessingTransportError from arr_processing.runner import RemoteRunSnapshot class OpenAgentProcessingTransport: """Uses only text Session/Run APIs; file resolution remains a runtime provider seam.""" def __init__( self, service: OpenAgentService, *, message_builder: Optional[Callable[[ProcessingRequest], str]] = None, ) -> None: self._service = service self._message_builder = message_builder or (lambda request: request.message()) def submit(self, request: ProcessingRequest, idempotency_key: str) -> RemoteRunSnapshot: conversation_id = self.conversation_id(request.job_id) try: response = self._service.send_message( conversation_id, self._message_builder(request), message_id=idempotency_key, external_subject_id=conversation_id, metadata={ "contract_version": PROCESSING_REQUEST_VERSION, "processing_kind": "opera_daily", }, ) except (OpenAgentAPIError, OpenAgentTransportError, OpenAgentProtocolError, AgentResponseError) as error: self._raise_mapped(error) return self._snapshot(response) def get(self, request: ProcessingRequest, remote_run_id: str) -> RemoteRunSnapshot: try: response = self._service.get_run( self.conversation_id(request.job_id), remote_run_id, ) except (OpenAgentAPIError, OpenAgentTransportError, OpenAgentProtocolError, AgentResponseError) as error: self._raise_mapped(error) return self._snapshot(response, expected_run_id=remote_run_id) def cancel(self, request: ProcessingRequest, remote_run_id: str) -> RemoteRunSnapshot: try: response = self._service.cancel_run( self.conversation_id(request.job_id), remote_run_id, ) except (OpenAgentAPIError, OpenAgentTransportError, OpenAgentProtocolError, AgentResponseError) as error: self._raise_mapped(error) return self._snapshot(response, expected_run_id=remote_run_id) @staticmethod def conversation_id(job_id: str) -> str: digest = hashlib.sha256(job_id.encode("utf-8")).hexdigest() return "arrproc_" + digest[:48] @staticmethod def _snapshot( response: Mapping[str, Any], expected_run_id: Optional[str] = None, ) -> RemoteRunSnapshot: if not isinstance(response, Mapping): raise ProcessingTransportError("PROCESSING_REMOTE_PROTOCOL_INVALID", retryable=False) run_id = response.get("run_id") status = response.get("status") final_content = response.get("final_content") if ( not isinstance(run_id, str) or not run_id or not isinstance(status, str) or not status or (expected_run_id is not None and run_id != expected_run_id) or (final_content is not None and not isinstance(final_content, str)) ): raise ProcessingTransportError("PROCESSING_REMOTE_PROTOCOL_INVALID", retryable=False) return RemoteRunSnapshot(run_id, status.lower(), final_content) @staticmethod def _raise_mapped(error: BaseException) -> None: if isinstance(error, OpenAgentAPIError): if error.active_run_conflict: raise ProcessingTransportError( "PROCESSING_ACTIVE_RUN_CONFLICT", retryable=True, active_run_conflict=True, ) from None if error.retryable: code = ( "PROCESSING_REMOTE_RATE_LIMITED" if error.status_code == 429 else "PROCESSING_REMOTE_UNAVAILABLE" ) raise ProcessingTransportError(code, retryable=True) from None raise ProcessingTransportError("PROCESSING_REMOTE_REJECTED", retryable=False) from None if isinstance(error, OpenAgentTransportError): raise ProcessingTransportError( "PROCESSING_REMOTE_UNAVAILABLE", retryable=True ) from None raise ProcessingTransportError( "PROCESSING_REMOTE_PROTOCOL_INVALID", retryable=False ) from None