Skip to content
Merged
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
27 changes: 27 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- **Controller contract metadata for Studio** — `server.register` now advertises `controller_contract_version`, `/api` base path, and canonical endpoint templates so Studio can route without hardcoded Supervaizer paths. The same contract is available at `GET /api/supervaizer/contract`.
- **DataResource request context** — DataResource callbacks can opt into a `context` keyword with workspace, mission, agent, and request identifiers while legacy callbacks continue to work unchanged.
- **CaseNodes update helpers** — `CaseNodes.node_index()` and `CaseNodes.make_update()` derive step indexes from the declared node set instead of requiring consumers to maintain a separate index map.

### Changed

- **DataResource fields can be marked sensitive** — `DataResourceField.sensitive` is included in registration payloads for Studio masking.

### Tests

- New: `tests/test_contracts.py` for contract endpoint templates, schema export, import safety, resolver behavior, and DataResource context headers.
- New: `tests/test_case.py` coverage for `CaseNodes.node_index()` and `CaseNodes.make_update()`.
- Updated: `tests/test_routes.py` to verify DataResource callbacks receive `DataResourceContext` from forwarded `X-Supervaize-*` headers.
- Updated: `tests/test_server.py` to assert registration info and `/api/supervaizer/contract` expose matching `/api` contract metadata.
- Deleted: none.

`just test`

| Status | Count |
| ---------- | ----- |
| ✅ Passed | 514 |
| 🤔 Skipped | 0 |
| 🔴 Failed | 0 |
| ⏱️ in | 01:03 |

## [0.15.1] - 2026-04-20

### Fixed
Expand Down
213 changes: 107 additions & 106 deletions src/supervaizer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,113 +4,114 @@
# If a copy of the MPL was not distributed with this file, you can obtain one at
# https://mozilla.org/MPL/2.0/.

# Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, you can obtain one at
# https://mozilla.org/MPL/2.0/.
"""Public Supervaizer SDK surface.

Exports are resolved lazily so contract-only consumers such as Studio can import
``supervaizer.contracts`` without loading the controller runtime.
"""

from __future__ import annotations

from importlib import import_module
from typing import Any

_EXPORTS: dict[str, tuple[str, str | None]] = {
"protocol": ("supervaizer.protocol", None),
"Account": ("supervaizer.account", "Account"),
"Agent": ("supervaizer.agent", "Agent"),
"AgentCustomMethodParams": ("supervaizer.agent", "AgentCustomMethodParams"),
"AgentMethod": ("supervaizer.agent", "AgentMethod"),
"AgentMethodField": ("supervaizer.agent", "AgentMethodField"),
"AgentMethodParams": ("supervaizer.agent", "AgentMethodParams"),
"AgentMethods": ("supervaizer.agent", "AgentMethods"),
"AgentResponse": ("supervaizer.agent", "AgentResponse"),
"FieldTypeEnum": ("supervaizer.agent", "FieldTypeEnum"),
"ApiError": ("supervaizer.common", "ApiError"),
"ApiResult": ("supervaizer.common", "ApiResult"),
"ApiSuccess": ("supervaizer.common", "ApiSuccess"),
"Case": ("supervaizer.case", "Case"),
"CaseNode": ("supervaizer.case", "CaseNode"),
"CaseNodes": ("supervaizer.case", "CaseNodes"),
"CaseNodeType": ("supervaizer.case", "CaseNodeType"),
"CaseNodeUpdate": ("supervaizer.case", "CaseNodeUpdate"),
"Cases": ("supervaizer.case", "Cases"),
"DataResource": ("supervaizer.data_resource", "DataResource"),
"DataResourceContext": ("supervaizer.data_resource", "DataResourceContext"),
"DataResourceField": ("supervaizer.data_resource", "DataResourceField"),
"Editable": ("supervaizer.data_resource", "Editable"),
"FieldType": ("supervaizer.data_resource", "FieldType"),
"AgentRegisterEvent": ("supervaizer.event", "AgentRegisterEvent"),
"CaseStartEvent": ("supervaizer.event", "CaseStartEvent"),
"CaseUpdateEvent": ("supervaizer.event", "CaseUpdateEvent"),
"Event": ("supervaizer.event", "Event"),
"JobFinishedEvent": ("supervaizer.event", "JobFinishedEvent"),
"JobStartConfirmationEvent": ("supervaizer.event", "JobStartConfirmationEvent"),
"ServerRegisterEvent": ("supervaizer.event", "ServerRegisterEvent"),
"EntityEvents": ("supervaizer.lifecycle", "EntityEvents"),
"EntityLifecycle": ("supervaizer.lifecycle", "EntityLifecycle"),
"EntityStatus": ("supervaizer.lifecycle", "EntityStatus"),
"Job": ("supervaizer.job", "Job"),
"JobContext": ("supervaizer.job", "JobContext"),
"JobInstructions": ("supervaizer.job", "JobInstructions"),
"JobResponse": ("supervaizer.job", "JobResponse"),
"Jobs": ("supervaizer.job", "Jobs"),
"Parameter": ("supervaizer.parameter", "Parameter"),
"ParametersSetup": ("supervaizer.parameter", "ParametersSetup"),
"Server": ("supervaizer.server", "Server"),
"create_error_response": ("supervaizer.server_utils", "create_error_response"),
"ErrorResponse": ("supervaizer.server_utils", "ErrorResponse"),
"ErrorType": ("supervaizer.server_utils", "ErrorType"),
"Telemetry": ("supervaizer.telemetry", "Telemetry"),
"TelemetryCategory": ("supervaizer.telemetry", "TelemetryCategory"),
"TelemetrySeverity": ("supervaizer.telemetry", "TelemetrySeverity"),
"TelemetryType": ("supervaizer.telemetry", "TelemetryType"),
"AgentMethodContract": ("supervaizer.contracts", "AgentMethodContract"),
"AgentMethodsContract": ("supervaizer.contracts", "AgentMethodsContract"),
"AgentRegistrationContract": ("supervaizer.contracts", "AgentRegistrationContract"),
"ControllerContract": ("supervaizer.contracts", "ControllerContract"),
"ControllerEndpoint": ("supervaizer.contracts", "ControllerEndpoint"),
"DataResourceContract": ("supervaizer.contracts", "DataResourceContract"),
"DataResourceFieldContract": ("supervaizer.contracts", "DataResourceFieldContract"),
"DynamicChoicesRequest": ("supervaizer.contracts", "DynamicChoicesRequest"),
"DynamicChoicesResponse": ("supervaizer.contracts", "DynamicChoicesResponse"),
"EventType": ("supervaizer.contracts", "EventType"),
"JobStartRequest": ("supervaizer.contracts", "JobStartRequest"),
"ServerRegistrationContract": (
"supervaizer.contracts",
"ServerRegistrationContract",
),
"build_data_resource_context_headers": (
"supervaizer.contracts",
"build_data_resource_context_headers",
),
"controller_contract_info": ("supervaizer.contracts", "controller_contract_info"),
"resolve_controller_endpoint": (
"supervaizer.contracts",
"resolve_controller_endpoint",
),
}

__all__ = sorted(_EXPORTS)


from supervaizer import protocol
from supervaizer.account import Account
from supervaizer.agent import (
Agent,
AgentCustomMethodParams,
AgentMethod,
AgentMethodParams,
AgentMethods,
AgentMethodField,
AgentResponse,
FieldTypeEnum,
)
from supervaizer.case import (
Case,
CaseNodeUpdate,
CaseNodeType,
Cases,
CaseNode,
CaseNodes,
)
from supervaizer.common import ApiError, ApiResult, ApiSuccess
from supervaizer.data_resource import (
DataResource,
DataResourceField,
Editable,
FieldType,
)
from supervaizer.event import (
AgentRegisterEvent,
CaseStartEvent,
CaseUpdateEvent,
Event,
EventType,
JobFinishedEvent,
JobStartConfirmationEvent,
ServerRegisterEvent,
)
from supervaizer.job import Job, JobContext, JobInstructions, JobResponse, Jobs
from supervaizer.lifecycle import EntityEvents, EntityLifecycle, EntityStatus
from supervaizer.parameter import Parameter, ParametersSetup
from supervaizer.server import Server
from supervaizer.server_utils import ErrorResponse, ErrorType, create_error_response
from supervaizer.telemetry import (
Telemetry,
TelemetryCategory,
TelemetrySeverity,
TelemetryType,
)
def _rebuild_forward_refs() -> None:
"""Resolve forward refs for models that depend on the public import order."""
try:
case_module = import_module("supervaizer.case")
agent_module = import_module("supervaizer.agent")
case_module.Case.model_rebuild()
agent_module.AgentResponse.model_rebuild()
except Exception:
pass

__all__ = [
"Account",
"Agent",
"AgentCustomMethodParams",
"AgentMethod",
"AgentMethodField",
"AgentMethodParams",
"AgentMethods",
"AgentRegisterEvent",
"ApiError",
"ApiResult",
"ApiSuccess",
"Case",
"CaseNode",
"CaseNodes",
"CaseNodeType",
"CaseNodeUpdate",
"Cases",
"CaseStartEvent",
"CaseUpdateEvent",
"create_error_response",
"DataResource",
"DataResourceField",
"Editable",
"FieldType",
"EntityEvents",
"EntityLifecycle",
"EntityStatus",
"ErrorResponse",
"ErrorType",
"Event",
"EventType",
"FieldTypeEnum",
"Job",
"JobContext",
"JobFinishedEvent",
"JobInstructions",
"JobResponse",
"Jobs",
"JobStartConfirmationEvent",
"Parameter",
"ParametersSetup",
"protocol",
"Server",
"ServerRegisterEvent",
"Telemetry",
"TelemetryCategory",
"TelemetrySeverity",
"TelemetryType",
]

# Rebuild models to resolve forward references after all imports are done
Case.model_rebuild()
AgentResponse.model_rebuild()
def __getattr__(name: str) -> Any:
if name not in _EXPORTS:
raise AttributeError(f"module 'supervaizer' has no attribute {name!r}")
module_name, attr_name = _EXPORTS[name]
module = import_module(module_name)
value = module if attr_name is None else getattr(module, attr_name)
globals()[name] = value
if module_name in {"supervaizer.agent", "supervaizer.case"}:
_rebuild_forward_refs()
return value
8 changes: 4 additions & 4 deletions src/supervaizer/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,11 +931,11 @@ def job_start(
if job_response.payload:
job.metadata = job_response.payload
# Send confirmation event now that metadata is populated
event = JobStartConfirmationEvent(
job=job,
account=server.supervisor_account,
)
if server.supervisor_account is not None:
event = JobStartConfirmationEvent(
job=job,
account=server.supervisor_account,
)
server.supervisor_account.send_event(sender=job, event=event)
else:
log.warning(
Expand Down
37 changes: 37 additions & 0 deletions src/supervaizer/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,33 @@ class CaseNodes(SvBaseModel):
def get(self, name: str) -> CaseNode | None:
return next((node for node in self.nodes if node.name == name), None)

def node_index(self, name: str, *, start: int = 1) -> int:
"""Return the stable 1-based index for a named case node."""
for offset, node in enumerate(self.nodes):
if node.name == name:
return start + offset
raise ValueError(f"Case node {name!r} not found")

def make_update(
self,
name: str,
*,
payload: Dict[str, Any] | None = None,
cost: float = 0.0,
is_final: bool = False,
upsert_to: str | None = None,
) -> CaseNodeUpdate:
"""Build a CaseNodeUpdate with an index derived from this node set."""
target_name = upsert_to or name
return CaseNodeUpdate(
name=name,
cost=cost,
payload=payload or {},
is_final=is_final,
index=self.node_index(target_name),
upsert=upsert_to is not None,
)

@property
def registration_info(self) -> Dict[str, Any]:
"""Returns registration info for the case nodes"""
Expand Down Expand Up @@ -520,3 +547,13 @@ def get_due_scheduled_steps(self) -> list[tuple]:
def __contains__(self, case_id: str) -> bool:
"""Check if case exists in any job's registry"""
return any(case_id in cases for cases in self.cases_by_job.values())


def rebuild_case_model_forward_refs() -> None:
"""Resolve runtime-only forward references used by Case models."""
from supervaizer.account import Account

Case.model_rebuild(_types_namespace={"Account": Account})


rebuild_case_model_forward_refs()
Loading
Loading