From c2624c87bea01420e323fe2698f8260802ec3f82 Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 22:54:00 +0300 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20feat:=20agent=20status=20endp?= =?UTF-8?q?oint=20and=20related=20tests-=20Add=20Controller.POST=5FAGENT?= =?UTF-8?q?=5FSTATUS=20and=20map=20to=20/api/supervaizer/agents/{agent=5Fs?= =?UTF-8?q?lug}/status=20the=20controller=20contract=20a=20dedicated=20sta?= =?UTF-8?q?tus=20route.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update contracts tests to assert new endpoint name resolved, ensuring API contract consistency. - Add HTTP POST / route handler response model changed to JobResponse and update the handler return type accordingly the endpoint returns standardized job statuss. - integration test that posts to the agent status endpoint and verifies_id, status and payload fields are returned; confirms agent.job is wired and serialized correctly. - Change logging in case methods from info to debug to avoid noisy logs normal operation. --- src/supervaizer/case.py | 4 ++-- src/supervaizer/contracts.py | 2 ++ src/supervaizer/routes.py | 19 +++++++++---------- tests/test_contracts.py | 12 ++++++++++++ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/supervaizer/case.py b/src/supervaizer/case.py index 25b67d7..87664ce 100644 --- a/src/supervaizer/case.py +++ b/src/supervaizer/case.py @@ -336,7 +336,7 @@ async def request_human_input( self, updateCaseNode: CaseNodeUpdate, message: str, **kwargs: Any ) -> None: updateCaseNode.index = len(self.updates) + 1 - log.info( + log.debug( f"[Update case human_input] CaseRef {self.case_ref} with update {updateCaseNode}" ) await self.account.send_update_case(self, updateCaseNode) @@ -350,7 +350,7 @@ def request_human_input_sync( self, updateCaseNode: CaseNodeUpdate, message: str, **kwargs: Any ) -> None: updateCaseNode.index = len(self.updates) + 1 - log.info( + log.debug( f"[Update case human_input] CaseRef {self.case_ref} with update {updateCaseNode}" ) self.account.send_update_case_sync(self, updateCaseNode) diff --git a/src/supervaizer/contracts.py b/src/supervaizer/contracts.py index 419be78..3d146eb 100644 --- a/src/supervaizer/contracts.py +++ b/src/supervaizer/contracts.py @@ -31,6 +31,7 @@ class ContractModel(BaseModel): class ControllerEndpoint(StrEnum): POST_AGENT_JOB_START = "POST_AGENT_JOB_START" POST_AGENT_JOB_CUSTOM = "POST_AGENT_JOB_CUSTOM" + POST_AGENT_STATUS = "POST_AGENT_STATUS" GET_JOB_STATUS = "GET_JOB_STATUS" GET_AGENT_JOB_STATUS = "GET_AGENT_JOB_STATUS" POST_AGENT_STOP = "POST_AGENT_STOP" @@ -53,6 +54,7 @@ class ControllerEndpoint(StrEnum): CONTROLLER_ENDPOINTS: dict[ControllerEndpoint, str] = { ControllerEndpoint.POST_AGENT_JOB_START: "/api/supervaizer/agents/{agent_slug}/jobs", ControllerEndpoint.POST_AGENT_JOB_CUSTOM: "/api/supervaizer/agents/{agent_slug}/custom/{method_name}", + ControllerEndpoint.POST_AGENT_STATUS: "/api/supervaizer/agents/{agent_slug}/status", ControllerEndpoint.GET_JOB_STATUS: "/api/supervaizer/jobs/{job_id}", ControllerEndpoint.GET_AGENT_JOB_STATUS: "/api/supervaizer/agents/{agent_slug}/jobs/{job_id}", ControllerEndpoint.POST_AGENT_STOP: "/api/supervaizer/agents/{agent_slug}/stop", diff --git a/src/supervaizer/routes.py b/src/supervaizer/routes.py index 486709b..380503b 100644 --- a/src/supervaizer/routes.py +++ b/src/supervaizer/routes.py @@ -881,8 +881,9 @@ async def stop_agent( "/status", summary=f"Get the status of the agent: {agent.name}", description="Get the status of the agent", + response_model=JobResponse, responses={ - http_status.HTTP_202_ACCEPTED: {"model": AgentResponse}, + http_status.HTTP_202_ACCEPTED: {"model": JobResponse}, }, dependencies=[ Depends(require_scope("write")) @@ -891,17 +892,15 @@ async def stop_agent( @handle_route_errors() async def status_agent( params: AgentMethodParams, agent: Agent = Depends(get_agent) - ) -> AgentResponse: + ) -> JobResponse: log.info(f"📥 POST /status [Status agent] {agent.name} with params {params}") result = agent.job_status(params.params) - return AgentResponse( - name=agent.name, - id=agent.id, - version=agent.version, - api_path=agent.path, - description=agent.description, - **result if result else {}, - ) + if result is None: + raise HTTPException( + status_code=http_status.HTTP_404_NOT_FOUND, + detail=f"Agent {agent.name} did not return a job status", + ) + return result @router.post( "/parameters", diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 46b2d62..b497b8e 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -30,6 +30,10 @@ def test_controller_contract_endpoints_are_api_prefixed() -> None: info["endpoints"]["POST_AGENT_JOB_START"] == "/api/supervaizer/agents/{agent_slug}/jobs" ) + assert ( + info["endpoints"]["POST_AGENT_STATUS"] + == "/api/supervaizer/agents/{agent_slug}/status" + ) assert ( info["endpoints"]["DATA_RESOURCE"] == "/api/agents/{agent_slug}/data/{resource_name}/" @@ -64,6 +68,14 @@ def test_resolve_controller_endpoint() -> None: ) == "/api/supervaizer/agents/agent-interviewer/jobs" ) + assert ( + resolve_controller_endpoint( + contract, + ControllerEndpoint.POST_AGENT_STATUS, + agent_slug="agent-interviewer", + ) + == "/api/supervaizer/agents/agent-interviewer/status" + ) assert ( resolve_controller_endpoint( contract.model_dump(mode="json"), From 5bab4377d49b1f641ec3427b59ddb7ab1406fcac Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 22:54:27 +0300 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20feat:=20agent=20status=20endp?= =?UTF-8?q?oint=20test=20job=20status=20responseAdd=20test=20that=20verifi?= =?UTF-8?q?es=20POST=20//{slug}/status=20returns=20the=20agent'sjob=5Fstat?= =?UTF-8?q?us=20as=20a=20JobResponse.=20The=20test=20patches=20Agent.job?= =?UTF-8?q?=5Fstatusreturn=20aResponse=20with=20job=5Fid,=20EntityStatus?= =?UTF-8?q?=5FPROGRESS,=20message,=20mounts=20the=20agents=20routes=20the?= =?UTF-8?q?=20app,=20sends=20a=20request=20withjob=5Fid=20and=20the200=20r?= =?UTF-8?q?esponse=20and=20that=20job=5Fid=20status=20andpayload=20fields?= =?UTF-8?q?=20match=20the=20patched=20response.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also import JobResponse and EntityStatus in the tests to construct theexpected response object and check the returned status string This ensures status endpoint correctly agent job status responses to the APIoutput. From 5801a48b41a8afa0997fe187503d13c005a1bb2b Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 22:55:53 +0300 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20feat(agents):=20add=20test=20?= =?UTF-8?q?for=20status=20endpoint=20returning=20job=20infoAdd=20new=20tes?= =?UTF-8?q?t=20that=20verifies=20POST=20/agents/{slug}/status=20returns=20?= =?UTF-8?q?theagent's=20job=5Fstatus=20response=20as=20a=20JobResponse=20w?= =?UTF-8?q?ith=20correct=20job=5Fidstatus,=20message,=20and=20payload.=20P?= =?UTF-8?q?atch=20Agent.job=5Fstatus=20return=20aJobResponse=20with=20Enti?= =?UTF-8?q?tyStatus.IN=5FPROGRESS=20and=20assert=20the=20API=20mapsthe=20s?= =?UTF-8?q?tatus=20and=20payload=20values=20correctly.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures the status route preserves and exposes jobmetadata (id, status, payload) for clients and guards againstregressions in status serialization. From 6e7541b4c7a877b9cb65bf95f0278a86252e7b00 Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 22:56:54 +0300 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20test=20for=20ag?= =?UTF-8?q?ent=20status=20endpoint=20job=5Fstatus=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 283e6eeddcdd3624b27eb9d6ee75b4ef2b252307 Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 22:57:09 +0300 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20test=20for=20ag?= =?UTF-8?q?ent=20status=20endpoint=20job=5Fstatus=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From ec747f39e3fe2e23bfc464c267127c3141c35e9a Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 22:58:23 +0300 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=A8=20feat:=20import=20JobResponse?= =?UTF-8?q?=20and=20EntityStatus=20for=20status=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From e026ec0ff89de376c19ef269de4e840a0859fef9 Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 23:02:32 +0300 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9C=A8=20feat:=20import=20JobResponse?= =?UTF-8?q?=20and=20EntityStatus=20for=20status=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 11de00dc2b00761d99c664c012cd83599991282a Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Sun, 3 May 2026 23:05:39 +0300 Subject: [PATCH 08/10] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20test=20for=20ag?= =?UTF-8?q?ent=20status=20endpoint=20job=5Fstatus=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_routes.py b/tests/test_routes.py index 18f2cc9..a099d92 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -21,9 +21,11 @@ AgentMethod, AgentMethods, Job, + JobResponse, Server, ) from supervaizer.data_resource import DataResource, DataResourceContext +from supervaizer.lifecycle import EntityStatus from supervaizer.parameter import ParametersSetup from supervaizer.routes import ( create_agents_routes, @@ -146,6 +148,39 @@ def mock_dynamic_choices( assert data["choices"]["projects"] == [["P1", "Project 1"], ["P2", "Project 2"]] +def test_agent_status_endpoint_returns_job_status_response( + server_fixture: Server, mocker: Any +) -> None: + """POST /status returns the agent job_status method response.""" + agent = server_fixture.agents[0] + mocker.patch( + "supervaizer.agent.Agent.job_status", + return_value=JobResponse( + job_id="job-123", + status=EntityStatus.IN_PROGRESS, + message="Campaign: in_progress", + payload={"campaign": {"status": "in_progress"}}, + ), + ) + + app = server_fixture.app + app.include_router(create_agents_routes(server_fixture)) + client = TestClient(app) + headers = {"X-API-Key": server_fixture.api_key or ""} + + resp = client.post( + f"/supervaizer/agents/{agent.slug}/status", + headers=headers, + json={"params": {"job_id": "job-123"}}, + ) + + assert resp.status_code == 200 + data = resp.json() + assert data["job_id"] == "job-123" + assert data["status"] == "in_progress" + assert data["payload"]["campaign"]["status"] == "in_progress" + + def test_dynamic_choices_endpoint_passes_workspace_slug_in_context( server_fixture: Server, ) -> None: From 969330436dad571bf3702bbc8f32fcf84402f1b3 Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Mon, 4 May 2026 00:43:24 +0300 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=A8=20off=20agent=20to=20worker=20t?= =?UTF-8?q?hreadsMove=20synchronous=20agent=20callbacks=20and=20hooks=20of?= =?UTF-8?q?f=20the=20event=20loop=20byexecuting=20them=20in=20background?= =?UTF-8?q?=20threads=20via.to=5Fthread.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Await dynamic_choices in routes.start using asyncio.to_thread to avoid blocking the loop computing choices. - Run agent.job_stop and agent.job_status in asyncio.to_thread from the HTTP POST handlers so synchronous user code cannot block request processing. - Update POST /status responses to include200/400/404/500 models clearer OpenAPI descriptions. - Document synchronous behavior in Agent._execute, job_stop and job_status docstrings to clarify that controller endpoints should call them from worker threads. This long-running user code from blocking the async server andpro API documentation for/response cases --- src/supervaizer/agent.py | 9 +++++++-- src/supervaizer/routes.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/supervaizer/agent.py b/src/supervaizer/agent.py index ba1b8d2..3fdb5ef 100644 --- a/src/supervaizer/agent.py +++ b/src/supervaizer/agent.py @@ -846,8 +846,11 @@ def update_parameters_from_server( log.debug("[No encrypted parameters] for {self.name}") def _execute(self, action: str, params: Dict[str, Any] = {}) -> JobResponse: - """ - Execute an agent method and return a JobResponse + """Execute an agent method and return a JobResponse. + + Runs synchronously in the caller's thread; HTTP entrypoints typically + offload this via ``asyncio.to_thread`` so user code cannot block the + event loop. """ module_name, func_name = action.rsplit(".", 1) @@ -975,12 +978,14 @@ def job_start( return job def job_stop(self, params: Dict[str, Any] = {}) -> Any: + """Synchronous stop hook; controller ``POST /stop`` runs it in a worker thread.""" if not self.methods or not self.methods.job_stop: raise ValueError("Agent methods not defined") method = self.methods.job_stop.method return self._execute(method, params) def job_status(self, params: Dict[str, Any] = {}) -> Any: + """Synchronous status hook; controller ``POST /status`` runs it in a worker thread.""" if not self.methods or not self.methods.job_status: raise ValueError("Agent methods not defined") method = self.methods.job_status.method diff --git a/src/supervaizer/routes.py b/src/supervaizer/routes.py index 380503b..6ff8006 100644 --- a/src/supervaizer/routes.py +++ b/src/supervaizer/routes.py @@ -705,7 +705,9 @@ async def get_dynamic_choices( "mission_id": body_params.get("mission_id"), } - choices = agent.dynamic_choices_callback("start", context) + choices = await asyncio.to_thread( + agent.dynamic_choices_callback, "start", context + ) log.info(f"📤 Agent {agent.name}: Dynamic choices keys: {list(choices.keys())}") return {"choices": choices} @@ -866,7 +868,7 @@ async def stop_agent( log.info(f"📥 POST /stop [Stop agent] {agent.name} with params {params}") # Pass job_context as 'context' parameter to match agent method expectations job_context = params.get("job_context", {}) - result = agent.job_stop({"context": job_context}) + result = await asyncio.to_thread(agent.job_stop, {"context": job_context}) res_info = result.registration_info if result else {} return AgentResponse( name=agent.name, @@ -883,7 +885,10 @@ async def stop_agent( description="Get the status of the agent", response_model=JobResponse, responses={ - http_status.HTTP_202_ACCEPTED: {"model": JobResponse}, + http_status.HTTP_200_OK: {"model": JobResponse}, + http_status.HTTP_400_BAD_REQUEST: {"model": ErrorResponse}, + http_status.HTTP_404_NOT_FOUND: {"model": ErrorResponse}, + http_status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponse}, }, dependencies=[ Depends(require_scope("write")) @@ -894,7 +899,7 @@ async def status_agent( params: AgentMethodParams, agent: Agent = Depends(get_agent) ) -> JobResponse: log.info(f"📥 POST /status [Status agent] {agent.name} with params {params}") - result = agent.job_status(params.params) + result = await asyncio.to_thread(agent.job_status, params.params) if result is None: raise HTTPException( status_code=http_status.HTTP_404_NOT_FOUND, From 00922f59bf6c1b2e769f9fd3e52ac651a4c76c2c Mon Sep 17 00:00:00 2001 From: Alain Prasquier Date: Mon, 4 May 2026 00:49:09 +0300 Subject: [PATCH 10/10] =?UTF-8?q?=E2=9C=A8=20feat(api):=20return=20JobResp?= =?UTF-8?q?onse=20and=20explicit=20codesUpdate=20status=20endpoint=20to=20?= =?UTF-8?q?use=20JobResponse=20as=20mainmodel=20and=20an=20explicit200=20s?= =?UTF-8?q?tatus=20in=20the=20route=20decoratorFastAPI=20the=20correct=20r?= =?UTF-8?q?esponse=20code=20default.=20AlignAPIschema=20reference=20JobRes?= =?UTF-8?q?ponse=20and=20add=20detailed=20error=20(400,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 404,500 referencing ErrorResponse improve API documentation andclient handling. This makes endpoint behavior and docsconsistent and clearer consumers --- docs/api/openapi.json | 28 ++++++++++++++++++++++++---- src/supervaizer/routes.py | 1 + 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 49fd224..c09260f 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1029,17 +1029,37 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentResponse" + "$ref": "#/components/schemas/JobResponse" } } } }, - "202": { - "description": "Accepted", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentResponse" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } diff --git a/src/supervaizer/routes.py b/src/supervaizer/routes.py index 6ff8006..bcf1799 100644 --- a/src/supervaizer/routes.py +++ b/src/supervaizer/routes.py @@ -884,6 +884,7 @@ async def stop_agent( summary=f"Get the status of the agent: {agent.name}", description="Get the status of the agent", response_model=JobResponse, + status_code=http_status.HTTP_200_OK, responses={ http_status.HTTP_200_OK: {"model": JobResponse}, http_status.HTTP_400_BAD_REQUEST: {"model": ErrorResponse},