diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index fcd5a990c1..6f2aa41345 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -79,6 +79,7 @@ jobs:
external/metaclass-registry/docs/source
external/PolyStore/README.md
external/PolyStore/docs/source
+ external/formbridge/README.md
external/pyqt-reactive/README.md
external/pyqt-reactive/docs
external/python-introspect/README.md
diff --git a/.gitmodules b/.gitmodules
index 64a89a88a5..4dd880c610 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -30,3 +30,7 @@
path = external/pyqt-reactive
url = https://github.com/OpenHCSDev/PyQT-reactive.git
branch = main
+[submodule "external/formbridge"]
+ path = external/formbridge
+ url = https://github.com/OpenHCSDev/formbridge.git
+ branch = main
diff --git a/README.md b/README.md
index dcb09fd10f..2ac6abf59f 100644
--- a/README.md
+++ b/README.md
@@ -183,7 +183,9 @@ graph TD
OH --> AB["ArrayBridge
(arrays)"]
OH --> PS["PolyStore
(I/O + streaming)"]
OH --> ZR["ZMQRuntime
(exec)"]
- OH --> QR["PyQT-reactive
(forms)"]
+ OH --> QR["PyQT-reactive
(Qt forms)"]
+ OH --> FB["formbridge
(form model + UI bridge)"]
+ QR --> FB
OS --> PI["python-introspect
(signatures)"]
OH --> MR["metaclass-registry
(plugins)"]
@@ -197,6 +199,7 @@ graph TD
| [**PolyStore**](https://github.com/OpenHCSDev/PolyStore) | Unified I/O & stream payloads | Generic storage and streaming payload primitives, backend lifecycle, virtual workspaces, atomic writes, format detection, and ROI extraction |
| [**ZMQRuntime**](https://github.com/OpenHCSDev/ZMQRuntime) | Process & transport runtime | Generic request, status, progress, cancellation, process-lifecycle, and viewer-control transport protocols |
| [**PyQT-reactive**](https://github.com/OpenHCSDev/PyQT-reactive) | UI form generation | React-style reactive forms from dataclasses with cross-window sync and flash animations |
+| [**formbridge**](https://github.com/OpenHCSDev/formbridge) | Toolkit-neutral forms and UI bridge | The form model, help and code documents, and the running-UI bridge contracts, client and server; imports no GUI toolkit, so headless MCP uses it without Qt |
| [**pycodify**](https://github.com/OpenHCSDev/pycodify) | Code ↔ object conversion | Python source as serialization format — type-preserving, diffable, editable, with collision handling |
| [**python-introspect**](https://github.com/OpenHCSDev/python-introspect) | Signature analysis | Pure-Python function/dataclass introspection for automatic UI generation and contract analysis |
| [**metaclass-registry**](https://github.com/OpenHCSDev/metaclass-registry) | Plugin discovery | Zero-boilerplate registry system powering microscope handler and storage backend auto-discovery |
diff --git a/benchmark/agent_validation/mcp_attempt_recorder.py b/benchmark/agent_validation/mcp_attempt_recorder.py
index f8f5efc98d..4789160e87 100644
--- a/benchmark/agent_validation/mcp_attempt_recorder.py
+++ b/benchmark/agent_validation/mcp_attempt_recorder.py
@@ -54,7 +54,7 @@
CustomFunctionRegistrationResult,
FunctionDetail,
)
-from openhcs.agent.dto.ui_bridge import UiCodeDocument
+from formbridge.bridge.dto import UiCodeDocument
from openhcs.core.function_patterns import normalize_function_pattern
from openhcs.core.function_reference import FunctionReference
from openhcs.core.pipeline_document import PipelineDocument, PipelineDocumentCodec
diff --git a/docs/refactor/consolidation-20261009/U4-formbridge.md b/docs/refactor/consolidation-20261009/U4-formbridge.md
new file mode 100644
index 0000000000..ceab6c7ade
--- /dev/null
+++ b/docs/refactor/consolidation-20261009/U4-formbridge.md
@@ -0,0 +1,102 @@
+# U4: formbridge, the toolkit-neutral tier
+
+**Head audited:** `openhcs` at `c7a953030` (PR #1203, E1, on `main` `eb64b3c13`); `pyqt-reactive` `main` at `1bce7de` (0.5.0); `formbridge` `main` at `7463390` (0.0.1 placeholder). **Rules:** [00-RULES.md](00-RULES.md). **Step 4** ([04-ARCHITECTURE.md](04-ARCHITECTURE.md#the-ui-one-session-interface-thin-renderers-surfaces-u1-to-u5), row U4).
+**Shared abstractions** ([02-SHARED-ABSTRACTIONS.md](02-SHARED-ABSTRACTIONS.md)). *Builds:* the `formbridge` library (repository `OpenHCSDev/formbridge`); `MainThreadDispatcher` with a queued and a Qt delivery; the bridge application declaration that replaces OpenHCS's bridge constants. *Uses:* `AutoRegisterMeta`, U2's `UiBridgeOperation` family, U3's `ScopeKey` and `FieldInspection`.
+
+This branch is stacked on E1 (#1203): E1 edits 342 files, including every UI-bridge module this surface moves, so building on `main` would leave each move in conflict with E1. Merge E1 first.
+
+## What is wrong
+
+**Headless MCP imports PyQt6, because toolkit-neutral code lives inside the Qt library and the generic UI-bridge DTOs, contracts and server live inside OpenHCS's GUI and agent packages.**
+
+Re-measured at `c7a953030`:
+
+- **Two B5 failures (owner U4).** `test_mcp_cold_build_does_not_import_pyqt6` and `test_hosted_mcp_cold_build_does_not_import_pyqt6` (`tests/unit/agent/test_mcp_distribution_runtime.py:165,194`) fail. `build_server()` loads `PyQt6.QtCore`, `QtGui`, `QtWidgets` and `sip` through two chains:
+ - `mcp/execution.py:12-18` imports `QCoreApplication`/`QEventLoop`/`QThread` and pyqt-reactive's `FutureCompletion` (a `QObject`) and `UiThreadDispatcher` (a Qt signal proxy). The headless server builds a `QCoreApplication` only to use its event loop as a main-thread queue.
+ - `authoring/session/pipelines.py:20` → `pyqt_reactive.services.function_pattern_code_document` → `pyqt_reactive.protocols` (package initializer) → `protocols/log_providers.py` → `pyqt_reactive.core` (package initializer) → `debounce_timer.py` (`QTimer`) and `reorderable_list_widget.py` (`QListWidget`). The code document and the provider registries are Qt-free; package initializers bundle them with widget adapters.
+- **pyqt-reactive's Qt-free tier is 10.8k lines in 64 modules** (0.5.0, measured by a static import graph: no module-level Qt import in the module or anything it imports, after dropping package initializers). It holds the form model (`forms/parameter_form_service.py` 924, `parameter_info_types.py` 350, `parameter_type_utils.py` 224, …), help documents and parameter help, the function-pattern code document (1,300), scope tokens, the pattern data manager, the provider registries in `protocols/`, the preview formatters and strategies, path cache, log discovery and streaming, the system-metrics sampler, ZMQ server scanning, and the Qt-free manager hooks in `widgets/shared/`. `animation/flash_trace.py` is an unused re-export shim of `flash_trace.py`.
+- **OpenHCS's headless code imports 24 of those modules from the Qt package** (`agent/dto/ui_bridge.py`, `agent/dto/viewer.py`, `agent/services/function_catalog_service.py`, `authoring/session/{pipelines,progress,session,step_scopes}.py`, `core/function_patterns.py`, `mcp/server.py`, `processing/custom_functions/events.py`, …).
+- **The generic UI-bridge tier is about 9k lines inside OpenHCS** (classified symbol by symbol from imports):
+ - `agent/dto/ui_bridge.py` (2,844): about 2.55k lines of identities, connection and descriptor DTOs, catalog pages, action, window, widget-tree, snapshot, ObjectState-field, code-document, branch and operation DTOs import nothing from OpenHCS's domain; about 230 lines (plate-manager, pipeline-editor, debug and live-measurement states, the selected-plate workflow) are OpenHCS's.
+ - `agent/services/ui_bridge_service.py` (1,832): the `UiBridgeOperation` family, `serves`/`UiBridgeOperationServer`, gateway ABC and errors, descriptor reading and resolution, and `UiBridgeService` are generic (about 1.73k); two operations (`selected_plate_workflow`, `session_events`), `UiBridgeEnvironment` and `viewer_launch_context` are OpenHCS's.
+ - `agent/services/ui_bridge_transport.py` (164), `agent/services/object_state_field_projection.py` (161), `agent/dto/common.py` (the `AgentError`/`AgentResultEnvelope`/`AgentCliRequest` family, 180), `core/selection.py` (99) and `agent/runtime_platform.py` (364) import no domain code.
+ - `pyqt_gui/services/ui_bridge_server.py` (623), `ui_bridge_contracts.py` (629), `ui_bridge_registry.py` (263) and most of `ui_agent_bridge.py` (about 1.2k of 1,916) and `ui_bridge_object_state.py` (about 780 of 1,163) import no Qt at all. They sit in the GUI package only because the DTOs did.
+ - `pyqt_gui/services/ui_bridge_windows.py` (2,894): window introspection, about 2.7k lines of generic Qt.
+- **OpenHCS constants stand in for an application declaration.** The protocol version `"openhcs.ui_bridge.v3"` (`ui_bridge_service.py:114`, read in five modules), `OPENHCS_ENDPOINT_APPLICATION`, `OPENHCS_ZMQ_CONFIG`, the `"OpenHCS", "ui-bridge"` runtime directory, the `OPENHCS_UI_BRIDGE_*` variable names, `"OpenHCSUiBridgeServer"`, `DEFAULT_AGENT_WINDOW_SNAPSHOT_DIR`, the hidden `"__plates__"` scope (`ui_bridge_object_state_scope_policy.py:17`) and `OpenHCSUiWindowId` aliases are read directly by generic code.
+- **Identity enums restate the identity registry** (`agent/dto/ui_bridge.py:124-154`: `_identity_enum` builds `UiCodeDocumentId`, `UiStateSurfaceId`, `UiWidgetId` from the declaration registries; only tests read them; rule 1a).
+- **Reassigned to U4 by U3 and U5:** the UI-bridge server and window introspection; the bridge widget id `plate_manager` and the dev client's `WorkflowTerminalStateCriterion`; `PyQtServiceAdapter.get_current_color_scheme` as a library protocol (called by pyqt-reactive `abstract_manager_widget.py:685`); the `QApplication` teardown segfault (about one run in five; the offscreen smoke test exits with `os._exit` to avoid it, `tests/pyqt_gui/test_main_window_offscreen_smoke.py:198`).
+- **`DualEditorWindow._current_function_spec`** (`pyqt_gui/windows/dual_editor_window.py:925`), "compatibility wrapper for tests", has no caller; neither has the `DualEditorSession.current_function_spec` it forwards to.
+
+## Target
+
+**formbridge 0.1.0** (no Qt, no OpenHCS; depends on python-introspect, objectstate, metaclass-registry, zmqruntime):
+
+- The Qt-free tier, keeping its module paths under `formbridge.` (`formbridge.forms.parameter_form_service`, `formbridge.services.help_document`, `formbridge.protocols.form_config`, …); the Qt-free manager hooks become `formbridge.managers.*`, the scope colour strategy `formbridge.theming.*`. Package initializers export nothing; every import names the defining module.
+- `formbridge.services.main_thread_dispatch`: `MainThreadDispatcher` (owned calls, atomic pre-start cancellation, close) with `QueuedMainThreadDispatcher`, which a headless process drains with `run_until(future)`. pyqt-reactive's `UiThreadDispatcher` subclasses it and supplies only Qt delivery.
+- `formbridge.bridge`: the generic UI-bridge tier listed above (DTOs, operation family, gateway, descriptors, transport, service, contracts, provider registry, in-process bridge and server). One `UiBridgeApplication` class declares what OpenHCS's constants restate (protocol version, endpoint application, transport config, runtime directory name, environment variable names, snapshot directory, server name); OpenHCS declares `OpenHCSUiBridgeApplication` once. Hidden scopes are a registered family; window identity aliases are declared on the identity classes. The identity enums are deleted.
+- The colour-scheme protocol is a formbridge ABC that pyqt-reactive's manager widgets read and OpenHCS's service adapter implements.
+
+**pyqt-reactive 0.6.0** depends on `formbridge>=0.1,<0.2`, deletes the moved modules (no re-exports) and receives the Qt half of the bridge: window introspection, the ObjectState code-document window providers, and the host-window declaration the introspection reads.
+
+**OpenHCS** keeps its declarations: its bridge application, widget, document and state-surface identities, the plate-manager, pipeline-editor, debug and live-measurement surfaces, the session operations, the selected-plate workflow and session-events operations, `OpenHCSUiAgentBridgeService`, and the MCP capabilities. `mcp/execution.py` runs on `QueuedMainThreadDispatcher`; no `QCoreApplication` exists in a headless process.
+
+Deleted: `pyqt_reactive.animation.flash_trace` (shim); `_identity_enum`, `UiCodeDocumentId`, `UiStateSurfaceId`, `UiWidgetId`; `DEFAULT_UI_BRIDGE_DESCRIPTOR_RESOLVER` and the module constants the application class replaces; `DualEditorWindow._current_function_spec` and `DualEditorSession.current_function_spec` (both unused).
+
+## Persisted state
+
+All runtime state, reset on upgrade (rule 2): formbridge's default data directories and log prefixes are named `formbridge` (OpenHCS configures its own); the pattern scope-token metadata key becomes `__formbridge_scope_token__`; UI-bridge descriptor files carry the application's protocol version.
+
+## Guards
+
+- `tests/unit/agent/test_mcp_distribution_runtime.py`: the two cold-build tests pass (stdio and hosted MCP import no PyQt6; hosted imports no `openhcs.pyqt_gui`).
+- `tests/unit/test_formbridge_guards.py`: importing the MCP server (stdio and hosted) or any formbridge module loads no PyQt6, PyQt5, PySide, qtpy or pyqt-reactive module; no module under `openhcs/` defines a moved name; no OpenHCS module imports a moved `pyqt_reactive` path.
+- formbridge `tests/test_no_toolkit_imports.py`: every formbridge module imports without a toolkit, and no formbridge source names one.
+
+## Tests
+
+- The pyqt-reactive tests of moved modules move to formbridge unchanged (14 files); the two that tested the flash-trace shim and a moved static gate are deleted or moved.
+- New: `formbridge/tests/test_main_thread_dispatch.py` (owner-thread execution, pre-start cancellation, callback-owned outcome, close).
+- The MCP affinity tests assert main-thread execution without Qt; the "no Qt application" case becomes "a worker call never runs on the worker".
+
+## New-case experiments
+
+- A second headless consumer of the form model or help documents (a web form renderer): today it imports a Qt package and loads PyQt6. After: `import formbridge.forms.parameter_form_service`.
+- A second application with a running-UI bridge: today it copies 9k lines out of OpenHCS. After: one `UiBridgeApplication` subclass, its identity declarations and its state-surface providers.
+
+## Done when
+
+The guards pass; the formbridge and pyqt-reactive PRs are open with CI on Linux, macOS and Windows for Python 3.11–3.13 plus ruff; OpenHCS points `external/formbridge` and `external/pyqt-reactive` at their branch heads with `formbridge>=0.1,<0.2` and `pyqt-reactive>=0.6,<0.7`; the agent, UI-bridge and pyqt_gui tests pass offscreen; the structural ratchet and NRA show no regression.
+
+## Outcome and corrections
+
+Measured against the E1 head `c7a953030`: OpenHCS production −13,730 +736; tests −520 +578. formbridge receives 10.8k lines from pyqt-reactive and about 9k from OpenHCS (more after `ruff format`); pyqt-reactive source −11,325 +3,378 (the Qt bridge half arrives, the Qt-free tier leaves). Guards: 5/5 in `tests/unit/test_formbridge_guards.py`, plus formbridge `test_no_toolkit_imports.py` and pyqt-reactive `test_only_qt_code.py`.
+
+- **The Qt-free UI-bridge server and most of the in-process bridge went to formbridge, not pyqt-reactive.** U3's reassignment expected the server beside the Qt code. It imports no Qt; only window introspection (`bridge.windows`) and the ObjectState code-document window providers (`bridge.object_state_documents`) are Qt, and they went to pyqt-reactive.
+- **Headless dispatch without Qt.** `mcp/execution.py` built a `QCoreApplication` only to use its event loop as a main-thread queue. formbridge's `QueuedMainThreadDispatcher` replaces it (`run_until(future)`); pyqt-reactive's `UiThreadDispatcher` is the Qt delivery of the same `MainThreadDispatcher`.
+- **One application declaration.** `UiBridgeApplication.current()` returns the one declared application (OpenHCS: `OpenHCSUiBridgeApplication`, imported by the OpenHCS bridge modules). Bridge envelopes carry formbridge's `SCHEMA_VERSION` (`formbridge.v1`); the bridge protocol is `openhcs.ui_bridge.v4`. Descriptor files are runtime state.
+- **Wire and MCP changes (rule 1).** `openhcs_ui_get_state_surface` now requires `surface_id` (the generic request cannot name OpenHCS's default surface); the MCP contract fixture records it. The bridge widget id is `datasets` (was `plate_manager`); its document and surfaces are `datasets.orchestrator_config`, `datasets.state`, `datasets.live_measurements`. Bridge error hints name operations instead of OpenHCS tool names.
+- **Rule 1a.** Window-snapshot capture scopes and frame conditions stored Qt callables in enum members; formbridge keeps the wire enums with Qt-free attributes, and each scope and observation kind has a registered Qt implementation. The dev client's `WorkflowTerminalStateCriterion` family restated `DatasetWorkflowOperation`; the terminal and failed row states now live on the operations.
+- **QApplication teardown segfault: not reproducible.** The offscreen smoke driver with normal interpreter teardown exited cleanly 18 of 18 times on this branch (6 after deleting the application, 12 with it alive) and 8 of 8 on the E1 head. The smoke test no longer exits with `os._exit`, so a teardown crash fails it.
+- **Found outside the brief:** `openhcs/utils/performance_monitor.py` was an unused copy of the library module (deleted); `parameter_ops_service` in pyqt-reactive had an `ImportError` stub fallback (deleted).
+- **Not done here:** `openhcs/core/log_utils.py` restates parts of formbridge's `core.log_utils` (log discovery and classification); owner U5 (dead and misplaced UI code). The GUI's own "plate manager" names (`PlateManagerWidget`, `OpenHCSUiWindowId.plate_manager`) are U1's container-manager rename.
+
+## Deliberate contract changes
+
+Merged with `main` at `44980dfe2` (E1, K2, K3, N1). Against the published MCP snapshot (`tests/fixtures/mcp/mcp_tool_contract.json` on `main`) exactly one tool record differs; surfaces and resources are identical.
+
+| Change | Where it shows | In the MCP snapshot |
+|---|---|---|
+| `openhcs_ui_get_state_surface` requires `surface_id` (the default `plate_manager.state` is gone) | MCP tool input schema | yes: the only snapshot change (3 lines added, 1 removed) |
+| Bridge protocol `openhcs.ui_bridge.v4` (was `v3`) | descriptor files and bridge request/response envelopes between an MCP client and a running GUI | no |
+| Bridge envelopes and bridge DTO results built by formbridge carry `schema_version: "formbridge.v1"`; OpenHCS's own DTOs keep `openhcs.agent.v1` | field value in bridge tool results | no (value, not schema) |
+| Bridge widget id `datasets` (was `plate_manager`); document `datasets.orchestrator_config`; surfaces `datasets.state`, `datasets.live_measurements` | identity values in bridge catalogs and requests | no (values) |
+| Bridge error hints name bridge operations (`get_operation_status`, `navigate_window`, `get_document`) instead of OpenHCS tool names | `hint` text of bridge errors | no |
+
+A GUI and an MCP client from different sides of this change refuse each other at the protocol check.
+
+## After central verification
+
+- **formbridge is declared wherever the other first-party libraries are:** `.gitmodules`, `pyproject.toml`, the README library table, `scripts/validate_docs.py`, the docs workflow's validation targets, and the test rosters that scan first-party library sources (configuration consumer coverage, configuration mirror deletion, U3's generic-Qt guards, help projection, the desktop update worker's source path). `openhcs/_source_dependencies.py`, `install_ci_candidate` and `validate_local_release_floors` discover `external/*` and needed no entry. The headless guard's subprocess imports `openhcs` first, so it resolves formbridge through the source-checkout activation and passes without formbridge installed.
+- **Gallery:** the scenario id is derived from the bridge identity, so the published asset and its two records were renamed together (`ui-plate-manager` → `ui-datasets`, `window_id` `datasets`). It is a documentation asset, not an external URL contract.
+- **Config leaves:** the consumers of `agent_bridge.*`, `list_previews.*` and `performance_monitor.*` now live in formbridge; the coverage scan and the mirror-deletion scans follow them there (formbridge added beside pyqt-reactive, paths of the five moved files updated). No scope was removed.
+- **nra-r1:** the local run used `--budget-seconds 600`; CI uses 160. The job timed out (no finding): the base scan took 141 s on that runner and the head scan re-parses the two library trees this surface changes.
diff --git a/external/ObjectState b/external/ObjectState
index 4eb8de1398..1fcdf1297f 160000
--- a/external/ObjectState
+++ b/external/ObjectState
@@ -1 +1 @@
-Subproject commit 4eb8de1398428c539e1e14360c847ccea2fe4b34
+Subproject commit 1fcdf1297f98217eba76f2e23423fcb0e488dc4a
diff --git a/external/PolyStore b/external/PolyStore
index 7ac9d01110..d367c60b8a 160000
--- a/external/PolyStore
+++ b/external/PolyStore
@@ -1 +1 @@
-Subproject commit 7ac9d011100b18f4ad1a54635ee94f52a47efb30
+Subproject commit d367c60b8afecc4c489580a31a865140b2c2f23a
diff --git a/external/formbridge b/external/formbridge
new file mode 160000
index 0000000000..b4d155842a
--- /dev/null
+++ b/external/formbridge
@@ -0,0 +1 @@
+Subproject commit b4d155842af281553bdf24cd1200c0856c1a62a1
diff --git a/external/pyqt-reactive b/external/pyqt-reactive
index 1bce7de2b3..4991fc732a 160000
--- a/external/pyqt-reactive
+++ b/external/pyqt-reactive
@@ -1 +1 @@
-Subproject commit 1bce7de2b33edd5b054f596da2b321c9b7cf93fd
+Subproject commit 4991fc732a9807ae54ada2b2ea9c9228f13bf9f8
diff --git a/external/zmqruntime b/external/zmqruntime
index dc327e6ca2..4826549299 160000
--- a/external/zmqruntime
+++ b/external/zmqruntime
@@ -1 +1 @@
-Subproject commit dc327e6ca2496aa4ad405479832f4e9e219750e4
+Subproject commit 4826549299f69ac56e3dfc858d15ba92c8574f95
diff --git a/openhcs/agent/capabilities.py b/openhcs/agent/capabilities.py
index 27a03c7018..163a5fe99a 100644
--- a/openhcs/agent/capabilities.py
+++ b/openhcs/agent/capabilities.py
@@ -35,12 +35,8 @@
AuthoringContext,
AuthoringContextRequest,
)
-from openhcs.agent.dto.common import (
- AGENT_PARAMETER_DESCRIPTION_METADATA_KEY,
- SCHEMA_VERSION,
- AgentDataclassCliRequest,
- RenderedSource,
-)
+from formbridge.contracts import AGENT_PARAMETER_DESCRIPTION_METADATA_KEY, AgentDataclassCliRequest
+from openhcs.agent.dto.common import SCHEMA_VERSION, RenderedSource
from openhcs.agent.dto.config import (
ConfigPatch,
ConfigRef,
@@ -81,7 +77,7 @@
)
from openhcs.authoring.session.operations import HeadlessOperation, SessionOperation
from openhcs.authoring.session.views import DatasetListView, PipelineStepsView
-from openhcs.agent.services.ui_bridge_service import (
+from formbridge.bridge.service import (
UiBridgeApplyDocumentOperation,
UiBridgeCloseWindowOperation,
UiBridgeFocusWindowOperation,
@@ -103,8 +99,6 @@
UiBridgeOperation,
UiBridgeRequestlessOperation,
UiBridgeRestoreSnapshotOperation,
- UiBridgeSelectedPlateWorkflowOperation,
- UiBridgeSessionEventsOperation,
UiBridgeSnapshotWindowOperation,
UiBridgeStatusOperation,
UiBridgeSwitchBranchOperation,
@@ -113,6 +107,10 @@
UiBridgeWaitForOperationReceiptOperation,
UiBridgeWidgetTreeOperation,
)
+from openhcs.agent.services.ui_bridge_service import (
+ UiBridgeSelectedPlateWorkflowOperation,
+ UiBridgeSessionEventsOperation,
+)
from openhcs.agent.dto.functions import (
CustomFunctionRegistrationRequest,
CustomFunctionRegistrationResult,
@@ -162,7 +160,7 @@
SyntheticPlateGenerationRequest,
SyntheticPlateGenerationResult,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiBridgeCatalog,
UiObjectStateFieldHelpQuery,
UiObjectStateFieldHelpResult,
diff --git a/openhcs/agent/dto/__init__.py b/openhcs/agent/dto/__init__.py
index af4711388a..d0033d795e 100644
--- a/openhcs/agent/dto/__init__.py
+++ b/openhcs/agent/dto/__init__.py
@@ -11,18 +11,11 @@
{
"openhcs.agent.dto.common": (
"SCHEMA_VERSION",
- "AgentError",
- "AgentResourceRef",
- "AgentWarning",
"RenderedSource",
),
"openhcs.agent.dto.mcp": (
"McpServerHealthResult",
),
- "pyqt_reactive.services.window_snapshot": (
- "WindowSnapshotCaptureScope",
- "WindowSnapshotCaptureSpec",
- ),
"openhcs.agent.dto.authoring": (
"AuthoringContext",
"AuthoringContextRequest",
@@ -178,115 +171,10 @@
"ViewerWindowValidationSummaryResult",
),
"openhcs.agent.dto.ui_bridge": (
- "UiActionCatalog",
- "UiActionIdentity",
- "UiActionInvocationStatus",
- "UiActionInvokeRequest",
- "UiActionInvokeResult",
- "UiActionSummary",
- "UiBranchCatalog",
- "UiBranchRef",
- "UiBranchSwitchRequest",
- "UiBridgeConfirmationRequirement",
- "UiBridgeConfirmationRequirementCarrier",
- "UiBridgeConnectionSpec",
- "UiBridgeCatalog",
- "UiBridgeDescriptorEnvelope",
- "UiBridgeDescriptorFile",
- "UiBridgeDescriptorSummary",
- "UiBridgeDescriptorWirePayload",
- "UiBridgeOperationIdentity",
- "UiBridgeOperationRef",
- "UiBridgeOperationStatusRequest",
- "UiBridgeOperationWaitRequest",
- "UiBridgeRequestEnvelope",
- "UiBridgeResponseEnvelope",
- "UiBridgeStatus",
- "UiCodeDocument",
- "UiCodeDocumentApplyRequest",
- "UiCodeDocumentApplyResult",
- "UiCodeDocumentCatalog",
- "UiCodeDocumentBaseRevision",
- "UiCodeDocumentCurrentRevision",
- "UiCodeDocumentId",
- "UiCodeDocumentOptionalBaseRevision",
- "UiCodeDocumentRequest",
- "UiCodeDocumentSummary",
- "UiCodeDocumentValidationRequest",
- "UiCodeDocumentValidationResult",
- "UiMutationReceipt",
- "UiMutationRequestToken",
- "UiMutationRequestTokenCarrier",
- "UiLiveOverviewItem",
- "UiLiveOverviewMetric",
- "UiLiveOverviewSection",
- "UiLiveOverviewSeverity",
- "UiLiveOverviewState",
- "UiObjectStateFieldListQuery",
- "UiObjectStateFieldListOptions",
- "UiObjectStateFieldListResult",
- "UiObjectStateFieldPathIndex",
- "UiObjectStateFieldProjection",
- "UiObjectStateFieldProvenance",
- "UiObjectStateFieldScopeProjection",
- "UiObjectStateFieldHelpQuery",
- "UiObjectStateFieldHelpRequest",
- "UiObjectStateFieldHelpResult",
- "UiObjectStateFieldMutationRequest",
- "UiObjectStateFieldMutationResult",
- "UiObjectStateFieldSummary",
- "UiObjectStateValuePreview",
- "UiObjectStateScopeCatalog",
- "UiObjectStateScopeIdentity",
- "UiObjectStateScopeListRequest",
- "UiObjectStateScopeSummary",
- "UiObjectStateScopeVisibility",
"UiPlateManagerState",
"UiSelectedPlateWorkflowKind",
"UiSelectedPlateWorkflowRequest",
"UiSelectedPlateWorkflowResult",
- "UiStateSurfaceDocument",
- "UiStateSurfaceEnvelope",
- "UiStateSurfaceCatalog",
- "UiStateSurfaceId",
- "UiStateSurfaceIdentity",
- "UiStateSurfaceRequest",
- "UiStateSurfaceSummary",
- "UiSemanticAddress",
- "UiSnapshotCatalog",
- "UiSnapshotListRequest",
- "UiSnapshotRef",
- "UiSnapshotRestoreRequest",
- "UiSnapshotRestoreResult",
- "UiTimeTravelRuntimeState",
- "UiTimeTravelHeadRequest",
- "UiWidgetIdentity",
- "UiWidgetActionInvokeRequest",
- "UiWidgetActionInvokeResult",
- "UiWidgetActionIssueCode",
- "UiWidgetActionSummary",
- "UiWidgetRect",
- "UiWidgetTreeNode",
- "UiWidgetTreeRequest",
- "UiWidgetTreeResult",
- "UiWindowCatalog",
- "UiWindowCloseRequest",
- "UiWindowCloseResult",
- "UiWindowFocusRequest",
- "UiWindowFocusResult",
- "UiWindowIdentity",
- "UiWindowManagerScope",
- "UiWindowNavigateRequest",
- "UiWindowNavigateResult",
- "UiWindowOpenPolicy",
- "UiWindowOperationRequest",
- "UiWindowSnapshotRequest",
- "UiWindowSnapshotResult",
- "UiWindowSummary",
- ),
- "openhcs.agent.ui_bridge_identities": (
- "MainWindowWidgetIdentity",
- "ManagedWindowWidgetIdentity",
),
},
)
diff --git a/openhcs/agent/dto/common.py b/openhcs/agent/dto/common.py
index 1cb84a95c4..101a44f0fb 100644
--- a/openhcs/agent/dto/common.py
+++ b/openhcs/agent/dto/common.py
@@ -2,201 +2,10 @@
from __future__ import annotations
-import argparse
-import inspect
-from abc import ABC, abstractmethod
-from collections.abc import Mapping
-from dataclasses import MISSING, dataclass, fields
-from functools import cache, wraps
-from typing import Self, cast, get_type_hints
-
-from python_introspect import (
- JsonObject,
- JsonValue,
- dataclass_from_mapping,
- to_jsonable,
-)
+from dataclasses import dataclass
SCHEMA_VERSION = "openhcs.agent.v1"
-AGENT_PARAMETER_DESCRIPTION_METADATA_KEY = "agent_parameter_description"
-AGENT_PARAMETER_PRODUCER_OUTPUT_CONTRACT_METADATA_KEY = (
- "agent_parameter_producer_output_contract"
-)
-
-
-@dataclass(frozen=True, slots=True)
-class AgentCliArgumentSpec:
- """CLI argument shape declared by agent request DTOs."""
-
- field_name: str
- flags: tuple[str, ...] = ()
- positional: bool = False
- nargs: str | int | None = None
- action: str | type[argparse.Action] | None = None
- help: str | None = None
-
-
-class AgentCliRequest(ABC):
- """Nominal request DTO that declares generated CLI argument projection."""
-
- @classmethod
- @abstractmethod
- def from_fields(cls, **kwargs) -> Self:
- raise NotImplementedError
-
- @classmethod
- def agent_cli_factory(cls):
- return cls.from_fields
-
- @classmethod
- def agent_cli_argument_specs(cls) -> tuple[AgentCliArgumentSpec, ...]:
- return ()
-
- @abstractmethod
- def as_tool_arguments(self) -> JsonObject:
- raise NotImplementedError
-
-
-class AgentDataclassCliRequest(AgentCliRequest):
- """CLI capability of a DTO whose declared fields are its tool input shape.
-
- Constructor reflection owns the signature; existing boundary codecs own
- nested reconstruction, constraints and JSON projection. Custom flattened
- request factories remain on their actual request owners.
- """
-
- @classmethod
- def from_fields(cls, **kwargs) -> Self:
- return dataclass_from_mapping(cls, kwargs)
-
- @classmethod
- def agent_cli_factory(cls):
- parameters = inspect.signature(cls).parameters
-
- @wraps(cls, updated=())
- def from_cli_fields(**kwargs):
- # Keep Python's constructor marker out of the typed wire codec.
- # Omitting that field lets its actual declared factory run once.
- for declared_field in fields(cls):
- if (
- declared_field.init
- and declared_field.default_factory is not MISSING
- and kwargs.get(declared_field.name)
- is parameters[declared_field.name].default
- ):
- kwargs.pop(declared_field.name)
- return cls.from_fields(**kwargs)
-
- return from_cli_fields
-
- @classmethod
- def agent_cli_argument_specs(cls) -> tuple[AgentCliArgumentSpec, ...]:
- annotations = get_type_hints(cls)
- return (
- *super().agent_cli_argument_specs(),
- *(
- AgentCliArgumentSpec(
- field_name=declared_field.name,
- action=argparse.BooleanOptionalAction,
- )
- for declared_field in fields(cls)
- if declared_field.init and annotations[declared_field.name] is bool
- ),
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return cast(JsonObject, to_jsonable(self))
-
-
-@dataclass(frozen=True, slots=True)
-class AgentWarning:
- code: str
- message: str
- hint: str | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class AgentError:
- code: str
- message: str
- hint: str | None = None
- exception_type: str | None = None
- path: str | None = None
-
- @classmethod
- def code_from_serialized_mapping(cls, value: Mapping[str, object]) -> str | None:
- """Project the error code through this DTO's declared field descriptor."""
-
- code = value.get(cls.code.__name__)
- return code if isinstance(code, str) else None
-
- @classmethod
- def from_exception(
- cls,
- code: str,
- exception: Exception,
- *,
- hint: str | None = None,
- path: str | None = None,
- ) -> "AgentError":
- return cls(
- code=code,
- message=str(exception),
- hint=hint,
- exception_type=type(exception).__name__,
- path=path,
- )
-
-
-@dataclass(frozen=True, kw_only=True)
-class AgentResultEnvelope:
- schema_version: str
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
- @classmethod
- @cache
- def serialized_errors_field_name(cls) -> str:
- """Derive the error-list wire field from the owning dataclass declaration."""
-
- type_hints = get_type_hints(cls)
- matching_fields = tuple(
- field.name
- for field in fields(cls)
- if type_hints[field.name] == tuple[AgentError, ...]
- )
- if len(matching_fields) != 1:
- raise TypeError(
- f"{cls.__name__} must declare exactly one AgentError tuple field."
- )
- return matching_fields[0]
-
- @classmethod
- def error_items_from_serialized_mapping(
- cls,
- value: Mapping[str, object],
- ) -> list[object] | None:
- """Return the serialized error list through the declared envelope field."""
-
- errors = value.get(cls.serialized_errors_field_name())
- return errors if isinstance(errors, list) else None
-
-
-@dataclass(frozen=True, kw_only=True)
-class AgentTimedStatusEnvelope(AgentResultEnvelope):
- status: str
- started_at_unix: float
-
-
-@dataclass(frozen=True, slots=True)
-class AgentResourceRef:
- uri: str
- title: str
- mime_type: str = "application/json"
- path: str | None = None
- size_bytes: int | None = None
- sha256: str | None = None
@dataclass(frozen=True, slots=True)
diff --git a/openhcs/agent/dto/config.py b/openhcs/agent/dto/config.py
index 70de65f39b..b84678ef63 100644
--- a/openhcs/agent/dto/config.py
+++ b/openhcs/agent/dto/config.py
@@ -5,11 +5,7 @@
from dataclasses import dataclass, field
from typing import Self
-from openhcs.agent.dto.common import (
- AgentCliArgumentSpec,
- AgentCliRequest,
- AgentResultEnvelope,
-)
+from formbridge.contracts import AgentCliArgumentSpec, AgentCliRequest, AgentResultEnvelope
from python_introspect import JsonObject
diff --git a/openhcs/agent/dto/execution.py b/openhcs/agent/dto/execution.py
index ff525b2613..fe6932cc5f 100644
--- a/openhcs/agent/dto/execution.py
+++ b/openhcs/agent/dto/execution.py
@@ -24,8 +24,8 @@
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
from zmqruntime.client import EndpointShutdownMode, EndpointShutdownResult
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import (
AgentCliArgumentSpec,
AgentCliRequest,
AgentError,
diff --git a/openhcs/agent/dto/execution_connection.py b/openhcs/agent/dto/execution_connection.py
index 8a63ac6ba5..4c7957f6f6 100644
--- a/openhcs/agent/dto/execution_connection.py
+++ b/openhcs/agent/dto/execution_connection.py
@@ -2,99 +2,27 @@
from __future__ import annotations
-from dataclasses import asdict, dataclass, field, replace
+from dataclasses import dataclass, field
from typing import TYPE_CHECKING
-from python_introspect import (
- JsonObject,
- project_dataclass,
- validate_annotated_dataclass,
-)
-from zmqruntime.config import NonBlankString, SocketPort, TransportMode
-from zmqruntime.transport import TransportEndpoint
+from formbridge.connection import EndpointConnectionFields, EndpointConnectionSpec
+from python_introspect import project_dataclass
-from openhcs.agent.dto.common import AgentDataclassCliRequest
-from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG, OpenHCSZMQConfig
+from openhcs.runtime.zmq_config import OpenHCSZMQConfig
if TYPE_CHECKING:
from openhcs.runtime.zmq_execution_client import ZMQExecutionClient
@dataclass(frozen=True, slots=True)
-class ExecutionConnectionSpec(AgentDataclassCliRequest):
- host: NonBlankString = "localhost"
- port: SocketPort | None = None
- transport_mode: TransportMode | None = None
- persistent: bool = True
-
- def __post_init__(self) -> None:
- validate_annotated_dataclass(self)
-
- def require_port(self, purpose: str) -> int:
- if self.port is None:
- raise ValueError(f"{purpose} requires an explicit port.")
- return self.port
-
- def transport_mode_value(self) -> str | None:
- return TransportMode.optional_to_text(self.transport_mode)
-
- def runtime_arguments(self) -> dict[str, object]:
- """Project exact typed constructor arguments for this connection."""
-
- return asdict(self.public_connection())
-
- def specified_runtime_arguments(self) -> dict[str, object]:
- """Project typed constructor arguments while omitting unset optionals."""
-
- return {
- name: value
- for name, value in self.runtime_arguments().items()
- if value is not None
- }
-
- def tool_arguments(self) -> JsonObject:
- """Project the canonical JSON-compatible tool argument fields."""
-
- arguments = self.runtime_arguments()
- arguments["transport_mode"] = self.transport_mode_value()
- return arguments
+class ExecutionConnectionSpec(EndpointConnectionSpec):
+ """Connection to an OpenHCS execution server."""
def public_connection(self) -> ExecutionConnectionSpec:
- """Return the credential-free base connection declaration."""
+ """Return the credential-free execution connection declaration."""
return project_dataclass(ExecutionConnectionSpec, self)
- def zmq_data_url(self, config: OpenHCSZMQConfig) -> str:
- return self.transport_endpoint(config, purpose="ZMQ data URL").data_url(config)
-
- def zmq_control_port(self, config: OpenHCSZMQConfig) -> int:
- return self.transport_endpoint(config, purpose="ZMQ control port").control_port(config)
-
- def zmq_control_url(self, config: OpenHCSZMQConfig) -> str:
- return self.transport_endpoint(config, purpose="ZMQ control URL").control_url(config)
-
- def transport_endpoint(
- self,
- config: OpenHCSZMQConfig = OPENHCS_ZMQ_CONFIG,
- *,
- purpose: str = "ZMQ transport endpoint",
- ) -> TransportEndpoint:
- """Project the exact route using the supplied execution config owner."""
-
- return config.client_endpoint(
- self.require_port(purpose),
- host=self.host,
- transport_mode=self.transport_mode,
- )
-
- def resolved(self, config: OpenHCSZMQConfig) -> ExecutionConnectionSpec:
- """Retain effective routing in the public nominal connection."""
-
- return replace(
- self.public_connection(),
- transport_mode=self.transport_endpoint(config).transport_mode,
- )
-
def execution_client(
self,
config: OpenHCSZMQConfig,
@@ -113,19 +41,7 @@ def execution_client(
@dataclass(frozen=True, kw_only=True)
-class ExecutionConnectionFields:
+class ExecutionConnectionFields(EndpointConnectionFields):
"""A request or result that carries one execution connection."""
connection: ExecutionConnectionSpec = field(default_factory=ExecutionConnectionSpec)
-
- @property
- def host(self) -> str:
- return self.connection.host
-
- @property
- def port(self) -> int | None:
- return self.connection.port
-
- @property
- def transport_mode(self) -> TransportMode | None:
- return self.connection.transport_mode
diff --git a/openhcs/agent/dto/functions.py b/openhcs/agent/dto/functions.py
index 3ab930d077..ba91801830 100644
--- a/openhcs/agent/dto/functions.py
+++ b/openhcs/agent/dto/functions.py
@@ -16,12 +16,8 @@
from zmqruntime.messages import MessageFields, ProcessIdentity, ResponseType
from zmqruntime.startup import EndpointStartupStatus
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
- AgentError,
- AgentDataclassCliRequest,
- AgentResultEnvelope,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentDataclassCliRequest, AgentResultEnvelope
from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec
from openhcs.agent.exceptions import AgentFacingErrorMixin
from openhcs.agent.path_policy import AgentPathPolicy
diff --git a/openhcs/agent/dto/knowledge.py b/openhcs/agent/dto/knowledge.py
index 50d65f8578..dbcbf41bab 100644
--- a/openhcs/agent/dto/knowledge.py
+++ b/openhcs/agent/dto/knowledge.py
@@ -6,11 +6,7 @@
from dataclasses import dataclass, replace
from typing import Self
-from openhcs.agent.dto.common import (
- AgentCliArgumentSpec,
- AgentCliRequest,
- AgentResultEnvelope,
-)
+from formbridge.contracts import AgentCliArgumentSpec, AgentCliRequest, AgentResultEnvelope
from python_introspect import JsonObject
diff --git a/openhcs/agent/dto/mcp.py b/openhcs/agent/dto/mcp.py
index 20eacf4665..126b2c490c 100644
--- a/openhcs/agent/dto/mcp.py
+++ b/openhcs/agent/dto/mcp.py
@@ -4,7 +4,7 @@
from dataclasses import dataclass
-from openhcs.agent.dto.common import AgentError, AgentTimedStatusEnvelope
+from formbridge.contracts import AgentError, AgentTimedStatusEnvelope
@dataclass(frozen=True, slots=True)
diff --git a/openhcs/agent/dto/pipeline.py b/openhcs/agent/dto/pipeline.py
index ce813cc3d3..958a9e3d80 100644
--- a/openhcs/agent/dto/pipeline.py
+++ b/openhcs/agent/dto/pipeline.py
@@ -5,7 +5,7 @@
from collections.abc import Mapping
from dataclasses import dataclass, field
-from openhcs.agent.dto.common import AgentResultEnvelope
+from formbridge.contracts import AgentResultEnvelope
from python_introspect import JsonObject, JsonValue
from openhcs.agent.dto.config import ConfigPatch
from openhcs.agent.dto.functions import FunctionIdentity
diff --git a/openhcs/agent/dto/plate.py b/openhcs/agent/dto/plate.py
index a4a288d82e..9079b42e4b 100644
--- a/openhcs/agent/dto/plate.py
+++ b/openhcs/agent/dto/plate.py
@@ -11,7 +11,7 @@
from metaclass_registry import AutoRegisterMeta
from zmqruntime.config import TransportMode
-from openhcs.agent.dto.common import (
+from formbridge.contracts import (
AGENT_PARAMETER_DESCRIPTION_METADATA_KEY,
AgentError,
AgentResultEnvelope,
diff --git a/openhcs/agent/dto/session.py b/openhcs/agent/dto/session.py
index 45e37b84cc..ffb953e16a 100644
--- a/openhcs/agent/dto/session.py
+++ b/openhcs/agent/dto/session.py
@@ -4,12 +4,8 @@
from dataclasses import dataclass, field
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
- AgentDataclassCliRequest,
- AgentError,
- AgentWarning,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentDataclassCliRequest, AgentError, AgentWarning
from enum import Enum
from python_introspect import validate_annotated_dataclass
@@ -166,6 +162,14 @@ class DatasetRowState:
"""The dataset's latest finished ordinary execution (its evidence id)."""
scope_accent_color: str | None = None
+ @property
+ def queued(self) -> bool:
+ return self.queue_position is not None
+
+ @property
+ def has_terminal_status(self) -> bool:
+ return self.terminal_status is not None
+
@dataclass(frozen=True, slots=True)
class DatasetListState:
diff --git a/openhcs/agent/dto/ui_bridge.py b/openhcs/agent/dto/ui_bridge.py
index 808abe0724..75dcb9eddb 100644
--- a/openhcs/agent/dto/ui_bridge.py
+++ b/openhcs/agent/dto/ui_bridge.py
@@ -1,139 +1,44 @@
-"""DTOs for the OpenHCS running-UI bridge agent API."""
+"""The OpenHCS UI-bridge state surfaces: datasets, pipeline editor, debug session, live measurements."""
from __future__ import annotations
-from abc import ABC, abstractmethod
-from collections.abc import Mapping
-from dataclasses import dataclass, field, replace
+from dataclasses import dataclass, field
from enum import Enum
-from math import isfinite
-from typing import TYPE_CHECKING, ClassVar, TypeVar
-
-from metaclass_registry import AutoRegisterMeta
-from objectstate import DottedFieldPath
-from pyqt_reactive.services.widget_tree_projection_config import (
- COMPACT_FIELD_PROJECTION_METADATA_KEY,
- CompactFieldProjection,
- WidgetNodeIdentity,
- WidgetTreeProjectionControls,
- WidgetTreeProjectionPolicy,
- always_project_compact_field,
- compact_dataclass_projection,
-)
-from pyqt_reactive.services.window_snapshot import (
- WindowSnapshotCaptureScope,
- WindowSnapshotCaptureSpec,
- WindowSnapshotFrameCondition,
- WindowVisualObservation,
-)
+from typing import TYPE_CHECKING
from python_introspect import (
JsonObject,
- JsonValue,
- overlay_non_none_dataclass,
- project_dataclass,
- to_jsonable,
- validate_annotated_dataclass,
-)
-from zmqruntime import EndpointApplication
-from zmqruntime.config import (
- NonBlankString,
- PositiveInteger,
- SocketPort,
- TransportMode,
)
-
-from openhcs.core.selection import AllItems, SelectedItems
+import openhcs.agent.ui_bridge_application # noqa: F401 (declares the OpenHCS bridge application)
from openhcs.agent.dto.session import DatasetRowState, PipelineStepState
-from openhcs.agent.dto.common import (
- AgentDataclassCliRequest,
+from formbridge.contracts import (
AGENT_PARAMETER_DESCRIPTION_METADATA_KEY,
AGENT_PARAMETER_PRODUCER_OUTPUT_CONTRACT_METADATA_KEY,
AgentCliRequest,
AgentError,
- AgentResourceRef,
- AgentResultEnvelope,
- AgentTimedStatusEnvelope,
AgentWarning,
)
-from openhcs.agent.dto.execution_connection import (
- ExecutionConnectionFields,
- ExecutionConnectionSpec,
-)
-from openhcs.agent.path_policy import DEFAULT_AGENT_WINDOW_SNAPSHOT_DIR
from openhcs.authoring.session.operations import SessionOperation
from openhcs.authoring.session.operations.datasets import DatasetWorkflowOperation
-from openhcs.agent.ui_bridge_identities import (
- MainWindowWidgetIdentity as MainWindowWidgetIdentity,
-)
-from openhcs.agent.ui_bridge_identities import (
- ManagedWindowWidgetIdentity as ManagedWindowWidgetIdentity,
-)
-from openhcs.agent.ui_bridge_identities import (
- PipelineDebugSessionStateSurfaceIdentityDeclaration as PipelineDebugSessionStateSurfaceIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- PipelineEditorStateSurfaceIdentityDeclaration as PipelineEditorStateSurfaceIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- PipelineEditorWidgetIdentity as PipelineEditorWidgetIdentity,
-)
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerOrchestratorCodeDocumentIdentity as PlateManagerOrchestratorCodeDocumentIdentity,
-)
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerStateSurfaceIdentityDeclaration as PlateManagerStateSurfaceIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerWidgetIdentity as PlateManagerWidgetIdentity,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiBridgeIdentityDeclaration as UiBridgeIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiCodeDocumentIdentityDeclaration as UiCodeDocumentIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiLiveOverviewStateSurfaceIdentityDeclaration as UiLiveOverviewStateSurfaceIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiOwnedByWidgetIdentityDeclaration as UiOwnedByWidgetIdentityDeclaration,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiStateSurfaceIdentityDeclarationBase as UiStateSurfaceIdentityDeclarationBase,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiWidgetIdentityDeclaration as UiWidgetIdentityDeclaration,
-)
-from openhcs.core.selection import (
+from openhcs.agent.ui_bridge_identities import DatasetListStateSurfaceIdentity
+from formbridge.selection import (
SelectedScopeIdsArgument,
SelectedScopeIdsCarrier,
- SelectionModeCarrier,
)
-
-UI_BRIDGE_UNKNOWN_OPERATION = "unknown"
-UI_BRIDGE_UNKNOWN_WIDGET = "unknown"
-_UiBridgeOperationSelectionT = TypeVar("_UiBridgeOperationSelectionT")
-
-
-class UiLiveOverviewSeverity(str, Enum):
- INFO = "info"
- WARNING = "warning"
- ERROR = "error"
-
-
-def _identity_enum(
- enum_name: str,
- identity_type: type[UiBridgeIdentityDeclaration],
-) -> type[Enum]:
- """Project one public string enum from registered UI identity declarations."""
- members = {
- declaration.enum_member_name: declaration.value
- for declaration in UiBridgeIdentityDeclaration.__registry__.values()
- if issubclass(declaration, identity_type)
- and declaration.enum_member_name is not None
- and declaration.value is not None
- }
- return Enum(enum_name, members, type=str)
+from formbridge.bridge.dto import (
+ UiActionCatalog,
+ UiActionInvokeResult,
+ UiBridgeConfirmationRequirement,
+ UiBridgeConfirmationRequirementCarrier,
+ UiCodeDocumentCurrentRevision,
+ UiCurrentSnapshotState,
+ UiMutationRequestToken,
+ UiMutationRequestTokenCarrier,
+ UiStateSurfaceEnvelope,
+)
+if TYPE_CHECKING:
+ from openhcs.core.progress.live_measurements import (
+ LiveMeasurementTablePreview,
+ )
def _dataset_workflow_enum() -> type[Enum]:
@@ -146,2395 +51,161 @@ def _dataset_workflow_enum() -> type[Enum]:
return Enum("UiSelectedPlateWorkflowKind", members, type=str)
-UiCodeDocumentId = _identity_enum("UiCodeDocumentId", UiCodeDocumentIdentityDeclaration)
-UiStateSurfaceId = _identity_enum(
- "UiStateSurfaceId",
- UiStateSurfaceIdentityDeclarationBase,
-)
-UiWidgetId = _identity_enum("UiWidgetId", UiWidgetIdentityDeclaration)
UiSelectedPlateWorkflowKind = _dataset_workflow_enum()
-class UiBridgeOperationStatus(ABC, metaclass=AutoRegisterMeta):
- """State of one bridge operation; each state is a subclass.
-
- ``name`` is the state's spelling in ``UiBridgeOperationRef.status``. A
- state that is not running or completed failed: it is terminal, reported as
- an error, and selects the caller's failed output.
- """
-
- __registry_key__ = "name"
- __skip_if_no_key__ = True
-
- name: ClassVar[str | None] = None
- is_terminal: ClassVar[bool] = True
- live_overview_severity: ClassVar[str] = UiLiveOverviewSeverity.ERROR.value
-
- @staticmethod
- def named(name: str) -> type[UiBridgeOperationStatus]:
- """Return the state spelled ``name``; ``ValueError`` when unknown."""
-
- status = UiBridgeOperationStatus.__registry__.get(name)
- if status is None:
- raise ValueError(f"Unknown UI bridge operation status {name!r}.")
- return status
-
- @classmethod
- def select_completion(
- cls,
- *,
- active: _UiBridgeOperationSelectionT,
- succeeded: _UiBridgeOperationSelectionT,
- failed: _UiBridgeOperationSelectionT,
- ) -> _UiBridgeOperationSelectionT:
- """Select caller-owned output for this state."""
-
- return failed
-
-
-class OperationRunning(UiBridgeOperationStatus):
- name = "running"
- is_terminal = False
- live_overview_severity = UiLiveOverviewSeverity.INFO.value
-
- @classmethod
- def select_completion(cls, *, active, succeeded, failed):
- return active
-
-
-class OperationCompleted(UiBridgeOperationStatus):
- name = "completed"
- live_overview_severity = UiLiveOverviewSeverity.INFO.value
-
- @classmethod
- def select_completion(cls, *, active, succeeded, failed):
- return succeeded
-
-
-class OperationFailed(UiBridgeOperationStatus):
- name = "failed"
-
-
-class OperationNotFound(UiBridgeOperationStatus):
- name = "not_found"
-
-
-class OperationUnavailable(UiBridgeOperationStatus):
- name = "unavailable"
-
-
-if TYPE_CHECKING:
- from openhcs.core.progress.live_measurements import (
- LiveMeasurementTablePreview,
- )
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiBridgeInstanceIdentity:
- bridge_instance_id: str | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiBridgeApplicationIdentity:
- """Application identity observed at a UI bridge boundary."""
-
- application: EndpointApplication | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiBridgeDescriptorFileRef:
- descriptor_file_path: str | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiCodeDocumentIdentity:
- document_id: str
-
- def as_document_identity(self) -> "UiCodeDocumentIdentity":
- if type(self) is UiCodeDocumentIdentity:
- return self
- return UiCodeDocumentIdentity(document_id=self.document_id)
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiStateSurfaceIdentity:
- surface_id: str
-
- def as_surface_identity(self) -> "UiStateSurfaceIdentity":
- if type(self) is UiStateSurfaceIdentity:
- return self
- return UiStateSurfaceIdentity(surface_id=self.surface_id)
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiWidgetIdentity:
- widget_id: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiSemanticAddress:
- """Stable semantic address shared by ObjectState, windows, and code surfaces."""
-
- object_state_scope_id: str
- field_path: str
- window_id: str | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiActionIdentity(UiWidgetIdentity):
- action_id: str
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiWindowIdentity:
- window_id: str
-
- def as_identity(self) -> "UiWindowIdentity":
- if type(self) is UiWindowIdentity:
- return self
- return UiWindowIdentity(window_id=self.window_id)
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiObjectStateScopeIdentity:
- object_state_scope_id: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateScopeVisibility:
- include_system_scopes: bool = False
-
- @classmethod
- def from_mapping(
- cls,
- value: Mapping[str, JsonValue] | None,
- ) -> "UiObjectStateScopeVisibility":
- if value is None:
- return cls()
- include_system_scopes = value.get("include_system_scopes")
- if isinstance(include_system_scopes, bool):
- return cls(include_system_scopes=include_system_scopes)
- return cls()
-
-
-class UiObjectStateFieldFilter(ABC, metaclass=AutoRegisterMeta):
- """Agent-facing filter over existing ObjectState field semantics.
-
- Each filter is a subclass; ``name`` is its spelling in tool arguments.
- """
-
- __registry_key__ = "name"
- __skip_if_no_key__ = True
-
- name: ClassVar[str | None] = None
-
- @staticmethod
- def named(name: str) -> type[UiObjectStateFieldFilter]:
- field_filter = UiObjectStateFieldFilter.__registry__.get(name)
- if field_filter is None:
- raise ValueError(
- f"Unsupported ObjectState field filter {name!r}; expected one of "
- f"{tuple(UiObjectStateFieldFilter.__registry__)!r}."
- )
- return field_filter
-
- @classmethod
- @abstractmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- """Return whether ``field`` passes this filter."""
-
-
-class AllFields(UiObjectStateFieldFilter):
- name = "all"
-
- @classmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- return True
-
-
-class DirtyFields(UiObjectStateFieldFilter):
- name = "dirty"
-
- @classmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- return field.dirty
-
-
-class DefaultDiffFields(UiObjectStateFieldFilter):
- name = "default_diff"
-
- @classmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- return field.signature_diff
-
-
-class InheritedFields(UiObjectStateFieldFilter):
- name = "inherited"
-
- @classmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- return field.inherited_value
-
-
-class RawResolvedFields(UiObjectStateFieldFilter):
- name = "raw_resolved"
-
- @classmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- return field.raw_value_is_none != field.resolved_value_is_none
-
-
-class SemanticFields(UiObjectStateFieldFilter):
- name = "semantic"
-
- @classmethod
- def matches(cls, field: "UiObjectStateFieldSummary") -> bool:
- return any(
- part.matches(field)
- for part in (DirtyFields, DefaultDiffFields, InheritedFields, RawResolvedFields)
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldListOptions:
- include_fields: bool = False
- field_limit: int = 200
- field_offset: int = 0
- include_field_values: bool = False
- include_field_descriptions: bool = False
- field_paths: tuple[str, ...] = ()
- field_filter: type[UiObjectStateFieldFilter] = AllFields
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiSnapshotIdentity:
- snapshot_id: str | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiCurrentSnapshotState:
- current_snapshot: "UiSnapshotRef | None" = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiCodeDocumentCurrentRevision:
- current_revision_token: str | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiCodeDocumentBaseRevision:
- base_revision_token: str = field(
- metadata={
- AGENT_PARAMETER_DESCRIPTION_METADATA_KEY: (
- "Base revision returned by the corresponding state/code-document "
- "read operation; this is not an action selection token."
- )
- }
- )
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiCodeDocumentOptionalBaseRevision:
- base_revision_token: str | None = field(
- default=None,
- metadata={
- AGENT_PARAMETER_DESCRIPTION_METADATA_KEY: (
- "Optional base state/code-document revision returned by the "
- "corresponding read operation; this is not an action selection "
- "token."
- )
- },
- )
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiTimeTravelRuntimeState:
- active: bool = False
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeConfirmationRequirement:
- """Explicit confirmation guard state for UI bridge mutations."""
-
- required: bool = True
-
- @classmethod
- def from_flag(cls, required: bool) -> "UiBridgeConfirmationRequirement":
- return cls(required=required)
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiBridgeConfirmationRequirementCarrier:
- """Shared confirmation policy behavior for UI bridge mutations."""
-
- confirmation_requirement: UiBridgeConfirmationRequirement = (
- UiBridgeConfirmationRequirement()
- )
-
- def confirmation_is_required(self) -> bool:
- return self.confirmation_requirement.required
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiBridgeConnectionFields(
- UiBridgeInstanceIdentity,
- UiBridgeDescriptorFileRef,
-):
- """Sparse connection-field projection for descriptor/env/MCP inputs."""
-
- host: NonBlankString | None = None
- port: SocketPort | None = None
- transport_mode: TransportMode | None = None
- persistent: bool | None = None
- timeout_ms: PositiveInteger | None = None
- auth_token: NonBlankString | None = None
-
- def __post_init__(self) -> None:
- validate_annotated_dataclass(self)
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeConnectionRequest(UiBridgeConnectionFields):
- """MCP-facing sparse connection request for a running UI bridge."""
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeConnectionSpec(
- ExecutionConnectionSpec,
- UiBridgeInstanceIdentity,
- UiBridgeDescriptorFileRef,
-):
- timeout_ms: PositiveInteger = 5000
- auth_token: NonBlankString | None = None
-
- @classmethod
- def from_fields(
- cls,
- fields: UiBridgeConnectionFields,
- defaults: "UiBridgeConnectionSpec | None" = None,
- ) -> "UiBridgeConnectionSpec":
- if defaults is None:
- defaults = cls()
- return overlay_non_none_dataclass(defaults, fields)
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeEndpointIdentity(
- ExecutionConnectionFields,
- UiBridgeInstanceIdentity,
- UiBridgeDescriptorFileRef,
-):
- """Exact public identity of one descriptor-backed UI bridge endpoint."""
-
- @classmethod
- def from_value(
- cls,
- value: ExecutionConnectionFields,
- ) -> UiBridgeEndpointIdentity:
- """Project identity from any declared UI bridge endpoint carrier."""
-
- return project_dataclass(
- cls,
- value,
- connection=value.connection.public_connection(),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeDescriptorEnvelope(
- UiBridgeApplicationIdentity,
- UiBridgeInstanceIdentity,
-):
- """Shared token-bearing descriptor metadata for UI bridge descriptor forms."""
-
- schema_version: str
- bridge_protocol_version: str
- pid: int
- started_at_unix: float
- auth_token: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeDescriptorFile(
- ExecutionConnectionFields,
- UiBridgeDescriptorEnvelope,
- UiBridgeDescriptorFileRef,
-):
- """Internal token-bearing descriptor-file payload."""
-
- def resolve_connection(
- self,
- defaults: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionSpec:
- """Overlay this descriptor's declared endpoint and bridge fields."""
-
- return overlay_non_none_dataclass(
- overlay_non_none_dataclass(defaults, self.connection),
- self,
- )
-
- def public_summary(self, status: str) -> UiBridgeDescriptorSummary:
- """Project this descriptor to its token-free public representation."""
-
- return project_dataclass(UiBridgeDescriptorSummary, self, status=status)
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeDescriptorWirePayload(
- ExecutionConnectionFields,
- UiBridgeDescriptorEnvelope,
-):
- """JSON descriptor file payload written by a running UI bridge."""
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeDescriptorSummary(
- AgentTimedStatusEnvelope,
- ExecutionConnectionFields,
- UiBridgeApplicationIdentity,
- UiBridgeInstanceIdentity,
- UiBridgeDescriptorFileRef,
-):
- pid: int
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeCatalog:
- schema_version: str
- bridges: tuple[UiBridgeDescriptorSummary, ...] = ()
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
@dataclass(frozen=True, slots=True)
-class UiBridgeStatus(
- ExecutionConnectionFields,
- UiBridgeInstanceIdentity,
- UiBridgeDescriptorFileRef,
+class UiPlateManagerState(
+ UiStateSurfaceEnvelope,
+ UiCodeDocumentCurrentRevision,
+ UiCurrentSnapshotState,
+ SelectedScopeIdsCarrier,
):
- schema_version: str
- reachable: bool
- service: str = "openhcs.ui_bridge"
- auth_required: bool = True
- supported_operations: tuple[str, ...] = ()
- provider_catalog_schema_versions: tuple[str, ...] = ()
- bridge_features: tuple[str, ...] = ()
- descriptor_status: str | None = None
- descriptors: tuple[UiBridgeDescriptorSummary, ...] = ()
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiSnapshotRef(UiSnapshotIdentity):
- schema_version: str
- index: int
- branch: str
- parent_snapshot_id: str | None
- timestamp_unix: float
- timestamp: str
- label: str
- num_states: int
- is_current: bool
- is_head: bool
- uri: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiBranchRef:
- schema_version: str
- name: str
- head_snapshot_id: str
- base_snapshot_id: str
- description: str
- is_current: bool
+ object_state_token: int
+ manager_execution_state: str
+ rows: tuple[DatasetRowState, ...]
@dataclass(frozen=True, slots=True)
-class UiCodeDocumentSummary(UiWidgetIdentity):
- schema_version: str
- identity: UiCodeDocumentIdentity
- title: str
- readable: bool
- writable: bool
- supported_selection_modes: tuple[str, ...] = ()
- current_selection_count: int = 0
- total_scope_count: int = 0
+class UiLiveMeasurementEntryState:
+ """One retained progress event paired with its authoritative table preview."""
- @property
- def document_id(self) -> str:
- return self.identity.document_id
+ sequence_id: int
+ execution_id: str
+ subject_id: str
+ axis_id: str
+ step_name: str
+ preview: LiveMeasurementTablePreview
+ truncated_preview_group: bool
-@dataclass(frozen=True, slots=True)
-class UiCodeDocumentCatalog:
- schema_version: str
- documents: tuple[UiCodeDocumentSummary, ...]
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiStateSurfaceSummary(UiWidgetIdentity):
- schema_version: str
- identity: UiStateSurfaceIdentity
- title: str
- readable: bool
- supported_selection_modes: tuple[str, ...] = ()
- current_selection_count: int = 0
- total_scope_count: int = 0
-
- @property
- def surface_id(self) -> str:
- return self.identity.surface_id
-
-
-@dataclass(frozen=True, slots=True)
-class UiStateSurfaceCatalog:
- schema_version: str
- surfaces: tuple[UiStateSurfaceSummary, ...]
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiCatalogPageMetadata:
- """Bounded catalog page metadata for growing UI bridge listings."""
-
- limit: int
- offset: int = 0
- returned_count: int = 0
- total_count: int | None = None
- truncated: bool = False
- next_offset: int | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiCatalogPageCarrier:
- """Inherited carrier for catalog responses that expose a nested page object."""
-
- page: UiCatalogPageMetadata | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiFieldCatalogPageCarrier:
- """Inherited carrier for field-list pagination nested under a scope summary."""
-
- field_page: UiCatalogPageMetadata | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiPageRequest:
- """Shared page request for UI bridge catalogs."""
-
- limit: int = 100
- offset: int = 0
-
-
-@dataclass(frozen=True, slots=True)
-class UiCodeDocumentRequest(
- UiCodeDocumentIdentity,
- SelectionModeCarrier,
- AgentCliRequest,
-):
- clean: bool = True
-
- @classmethod
- def from_fields(
- cls,
- *,
- document_id: str,
- selection_mode: str = SelectedItems.name,
- clean: bool = True,
- ) -> "UiCodeDocumentRequest":
- return cls(
- document_id=document_id,
- selection_mode=selection_mode,
- clean=clean,
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "document_id": self.document_id,
- "selection_mode": self.selection_mode,
- "clean": self.clean,
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiStateSurfaceRequest(
- UiStateSurfaceIdentity,
- SelectionModeCarrier,
- UiCodeDocumentOptionalBaseRevision,
- AgentCliRequest,
-):
- @classmethod
- def from_fields(
- cls,
- *,
- surface_id: str = PlateManagerStateSurfaceIdentityDeclaration.require_value(),
- selection_mode: str = AllItems.name,
- base_revision_token: str | None = None,
- ) -> "UiStateSurfaceRequest":
- return cls(
- surface_id=surface_id,
- selection_mode=selection_mode,
- base_revision_token=base_revision_token,
- )
-
- def as_tool_arguments(self) -> JsonObject:
- """Project the declared state-surface factory fields to MCP arguments."""
-
- return {
- "surface_id": self.surface_id,
- "selection_mode": self.selection_mode,
- "base_revision_token": self.base_revision_token,
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiCodeDocument(
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- schema_version: str
- summary: UiCodeDocumentSummary
- source: str
- mime_type: str
- size_bytes: int
- sha256: str
- warnings: tuple[AgentWarning, ...] = ()
- errors: tuple[AgentError, ...] = ()
-
-
-
-
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiStateSurfaceEnvelope(AgentResultEnvelope):
- """Shared envelope for typed UI state-surface payloads."""
-
- summary: UiStateSurfaceSummary
- unchanged: bool = False
-
-
-@dataclass(frozen=True, slots=True)
-class UiPlateManagerState(
- UiStateSurfaceEnvelope,
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- object_state_token: int
- manager_execution_state: str
- rows: tuple[DatasetRowState, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveMeasurementEntryState:
- """One retained progress event paired with its authoritative table preview."""
-
- sequence_id: int
- execution_id: str
- subject_id: str
- axis_id: str
- step_name: str
- preview: LiveMeasurementTablePreview
- truncated_preview_group: bool
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveMeasurementsState(
- UiStateSurfaceEnvelope,
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- """Bounded live measurement tables retained by the running Plate Manager."""
-
- object_state_token: int
- retained_entry_count: int
- visible_entry_count: int
- total_row_count: int
- latest_sequence_id: int | None
- entries: tuple[UiLiveMeasurementEntryState, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiPipelineEditorState(
- UiStateSurfaceEnvelope,
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- object_state_token: int
- current_plate_scope_id: str | None
- pipeline_scope_id: str | None
- steps: tuple[PipelineStepState, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiDebugActionState(SelectedScopeIdsCarrier):
- action_id: str
- label: str
- placement: str
- enabled: bool
- side_effects: tuple[str, ...]
- confirmation_required: bool
- requires_active_debug_session: bool
- disabled_error: AgentError | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiDebugCursorState:
- step_index: int
- step_scope_id: str | None
- group_key: str | None
- invocation_key: str | None
- pattern_group_identity: str | None
- dirty: bool = False
-
-
-@dataclass(frozen=True, slots=True)
-class UiDebugTerminalSummaryState:
- debug_session_id: str
- plate_scope_id: str
- terminal_status: str
- command_type: str | None
- axis_id: str | None
- snapshot_id: str | None
- snapshot_store_ref: str | None
- snapshot_store_backend: str | None
- step_name: str | None
- callable_name: str | None
- cursor: UiDebugCursorState | None
- completed_at_unix: float | None
-
-
-@dataclass(frozen=True)
-class UiDebugSessionIdentityCarrier:
- debug_session_id: str
-
-
-@dataclass(frozen=True)
-class UiDebugSnapshotStoreRefCarrier:
- snapshot_store_ref: str | None
- snapshot_store_backend: str | None
-
-
-@dataclass(frozen=True, slots=True)
-class UiProgressIdentityState:
- execution_id: str
- subject_id: str
- axis_id: str
- step_name: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiDebugRuntimeFrameState(
- UiDebugSessionIdentityCarrier,
- UiDebugSnapshotStoreRefCarrier,
-):
- progress_identity: UiProgressIdentityState
- cursor: UiDebugCursorState
- event_type: str
- step_name: str
- callable_name: str | None
- snapshot_id: str | None
- timestamp: float
-
-
-@dataclass(frozen=True, slots=True)
-class UiPipelineDebugSessionState(
- UiStateSurfaceEnvelope,
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- object_state_token: int
- current_plate_scope_id: str | None
- pipeline_scope_id: str | None
- manager_execution_state: str
- initialized: bool
- compiled: bool
- phase: str
- active_session_id: str | None
- execution_id: str | None
- axis_id: str | None
- selected_source_group: str | None
- snapshot_store_ref: str | None
- snapshot_store_backend: str | None
- terminal_status: str | None
- cursor: UiDebugCursorState | None
- terminal_summary: UiDebugTerminalSummaryState | None
- actions: tuple[UiDebugActionState, ...]
- current_frame: UiDebugRuntimeFrameState | None = None
- last_frame: UiDebugRuntimeFrameState | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveOverviewMetric:
- key: str
- label: str
- value: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveOverviewItem:
- label: str
- status: str | None = None
- detail: str | None = None
- severity: str = UiLiveOverviewSeverity.INFO.value
- source_surface_id: str | None = None
- source_window_id: str | None = None
- source_widget_id: str | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveOverviewSection:
- section_id: str
- title: str
- summary: str
- metrics: tuple[UiLiveOverviewMetric, ...] = ()
- items: tuple[UiLiveOverviewItem, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveOverviewState(
- UiStateSurfaceEnvelope,
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- object_state_token: int
- sections: tuple[UiLiveOverviewSection, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiStateSurfaceDocument(
- UiStateSurfaceEnvelope,
- UiCodeDocumentCurrentRevision,
- UiCurrentSnapshotState,
- SelectedScopeIdsCarrier,
-):
- payload_schema: str
- payload: JsonObject
-
-
-class UiActionInvocationStatus(ABC, metaclass=AutoRegisterMeta):
- """Whether the UI took a requested action; ``name`` is its wire spelling."""
-
- __registry_key__ = "name"
- __skip_if_no_key__ = True
-
- name: ClassVar[str | None] = None
- accepted: ClassVar[bool] = False
-
-
-class ActionAccepted(UiActionInvocationStatus):
- name = "accepted"
- accepted = True
-
-
-class ActionRejected(UiActionInvocationStatus):
- name = "rejected"
-
-
-class ActionUnavailable(UiActionInvocationStatus):
- name = "unavailable"
-
-
-class UiWidgetActionInvocationOutcome(str, Enum):
- """Closed terminal outcomes for one projected-widget action."""
-
- invoked: bool
-
- INVOKED = ("invoked", True)
- NOT_INVOKED = ("not_invoked", False)
-
- def __new__(
- cls,
- value: str,
- invoked: bool,
- ) -> "UiWidgetActionInvocationOutcome":
- member = str.__new__(cls, value)
- member._value_ = value
- member.invoked = invoked
- return member
-
- @classmethod
- def from_invoked(cls, invoked: bool) -> "UiWidgetActionInvocationOutcome":
- return cls.INVOKED if invoked else cls.NOT_INVOKED
-
-
-@dataclass(frozen=True, slots=True)
-class UiMutationRequestToken:
- value: str | None = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiMutationRequestTokenCarrier:
- """Shared request-token carrier for bridge mutation requests."""
-
- request_token: UiMutationRequestToken = UiMutationRequestToken()
-
-
-@dataclass(frozen=True, slots=True)
-class UiMutationReceipt:
- request_token: UiMutationRequestToken
- bridge_operation_id: str | None = None
- accepted: bool = False
-
- @classmethod
- def accepted_for(
- cls,
- request_token: UiMutationRequestToken,
- *,
- bridge_operation_id: str | None = None,
- ) -> "UiMutationReceipt":
- return cls(
- request_token=request_token,
- bridge_operation_id=bridge_operation_id,
- accepted=True,
- )
-
- @classmethod
- def rejected_for(
- cls,
- request_token: UiMutationRequestToken,
- *,
- bridge_operation_id: str | None = None,
- ) -> "UiMutationReceipt":
- return cls(
- request_token=request_token,
- bridge_operation_id=bridge_operation_id,
- accepted=False,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiActionSummary(SelectionModeCarrier):
- schema_version: str
- identity: UiActionIdentity
- title: str
- enabled: bool
- invocation_mode: str
- side_effects: tuple[str, ...] = ()
- disabled_error: AgentError | None = None
- confirmation_required: bool = False
- current_selection_count: int = 0
- target_scope_ids: tuple[str, ...] = ()
- selection_revision_token: str | None = None
- related_state_surface_ids: tuple[str, ...] = ()
- required_target_count: int | None = None
-
- def invocation_guard_error(
- self,
- request: UiActionInvokeRequest,
- ) -> AgentError | None:
- """Validate an invocation against this action's authoritative summary."""
- if not self.enabled:
- return self.disabled_error or AgentError(
- code="ui_action_disabled",
- message=f"UI action {request.action_id!r} is disabled.",
- )
- if self.confirmation_required and request.confirmation_is_required():
- return AgentError(
- code="confirmation_required",
- message=(
- "This UI action mutates state; set require_confirmation=False "
- "to dispatch it."
- ),
- )
- if (
- self.required_target_count is not None
- and len(request.selected_scope_ids) != self.required_target_count
- ):
- return AgentError(
- code="ui_action_target_required",
- message=(
- f"UI action {request.action_id!r} requires exactly "
- f"{self.required_target_count} target scope(s)."
- ),
- )
- if self.required_target_count is not None:
- unavailable_targets = set(request.selected_scope_ids) - set(
- self.target_scope_ids
- )
- if unavailable_targets:
- return AgentError(
- code="unknown_ui_action_target",
- message="Requested target scope is not available for this UI action.",
- )
- elif (
- request.selected_scope_ids
- and request.selected_scope_ids != self.target_scope_ids
- ):
- return AgentError(
- code="stale_ui_action_selection",
- message="Requested target scopes do not match current UI action targets.",
- hint=self.selection_bound_hint(request),
- )
- if (
- request.observed_selection_revision_token is not None
- and self.selection_revision_token is not None
- and request.observed_selection_revision_token
- != self.selection_revision_token
- ):
- return AgentError(
- code="stale_ui_action_revision",
- message="UI action selection changed after the action was planned.",
- hint=(
- "Call openhcs_ui_list_actions again for this widget and action, "
- "then pass its selection_revision_token as "
- "observed_selection_revision_token. State-surface "
- "base_revision_token values belong to a different token family."
- ),
- )
- return None
-
- def selection_bound_hint(self, request: UiActionInvokeRequest) -> str:
- """Explain how an invocation relates to the action's current targets."""
- requested_targets = ", ".join(request.selected_scope_ids)
- current_targets = ", ".join(self.target_scope_ids) or ""
- parts = [
- (
- f"{request.widget_id}/{request.action_id} is selection-bound "
- f"(selection_mode={self.selection_mode or ''}); "
- "--target-scope-id is a staleness guard for the current UI "
- "selection, not direct ObjectState navigation."
- ),
- f"Current action targets: {current_targets}.",
- ]
- if self.related_state_surface_ids:
- parts.append(
- "Read related state surfaces before invoking: "
- f"{', '.join(self.related_state_surface_ids)}."
- )
- if len(request.selected_scope_ids) == 1:
- parts.append(
- "For a list-backed manager, select the desired item with "
- "openhcs_ui_navigate_window using "
- f"window_id={self.identity.widget_id!r} and item_id from its "
- "current state surface. "
- f"The requested target scope is {requested_targets!r}. "
- "Then read the action again for its current selection revision."
- )
- else:
- parts.append(
- "Change the selection in the owning manager, then read the action "
- "again for its current targets and selection revision."
- )
- return " ".join(parts)
-
-
-@dataclass(frozen=True, slots=True)
-class UiActionCatalog(UiCatalogPageCarrier):
- schema_version: str
- actions: tuple[UiActionSummary, ...]
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiActionInvokeRequest(
- UiActionIdentity,
- SelectedScopeIdsCarrier,
- UiBridgeConfirmationRequirementCarrier,
- UiMutationRequestTokenCarrier,
- AgentCliRequest,
-):
- observed_selection_revision_token: str | None = field(
- default=None,
- metadata={
- AGENT_PARAMETER_DESCRIPTION_METADATA_KEY: (
- "Action-selection revision returned as selection_revision_token by "
- "the action-catalog producer for the same widget, action, and target "
- "selection; do not pass a state-surface revision token."
- ),
- AGENT_PARAMETER_PRODUCER_OUTPUT_CONTRACT_METADATA_KEY: UiActionCatalog,
- },
- )
-
- @classmethod
- def from_fields(
- cls,
- *,
- widget_id: str,
- action_id: str,
- target_scope_ids: list[str] | None = None,
- observed_selection_revision_token: str | None = None,
- request_token: str | None = None,
- require_confirmation: bool = True,
- ) -> "UiActionInvokeRequest":
- selected_scope_ids = SelectedScopeIdsArgument.from_optional_iterable(
- target_scope_ids
- )
- return cls(
- widget_id=widget_id,
- action_id=action_id,
- selected_scope_ids=selected_scope_ids.selected_scope_ids,
- observed_selection_revision_token=observed_selection_revision_token,
- request_token=UiMutationRequestToken(value=request_token),
- confirmation_requirement=UiBridgeConfirmationRequirement.from_flag(
- require_confirmation
- ),
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "widget_id": self.widget_id,
- "action_id": self.action_id,
- "target_scope_ids": list(self.selected_scope_ids),
- "observed_selection_revision_token": (
- self.observed_selection_revision_token
- ),
- "request_token": self.request_token.value,
- "require_confirmation": self.confirmation_is_required(),
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiActionInvokeResult:
- schema_version: str
- identity: UiActionIdentity
- status: type[UiActionInvocationStatus]
- receipt: UiMutationReceipt
- target_scope_ids: tuple[str, ...] = ()
- selection_revision_token: str | None = None
- workflow_status_surface_ids: tuple[str, ...] = ()
- event_sequence: int | None = field(
- default=None,
- metadata={
- AGENT_PARAMETER_DESCRIPTION_METADATA_KEY: (
- "Session event sequence the action started at; read the work it "
- "started with openhcs_ui_session_events(after_sequence=...). "
- "None when the action started no session work."
- ),
- },
- )
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowManagerScope:
- """WindowManager scope token associated with a UI window identity."""
-
- value: str
-
- @classmethod
- def from_identity(cls, identity: UiWindowIdentity) -> "UiWindowManagerScope":
- return cls(value=identity.window_id)
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWindowSemanticCarrier:
- """Shared semantic state carried by agent-visible window summaries."""
-
- object_state_scope_id: str | None = None
- dirty: bool = False
- signature_diff: bool = False
- dirty_field_count: int = 0
- signature_diff_field_count: int = 0
- semantic_markers: tuple[str, ...] = ()
- managed_action_ids: tuple[str, ...] = ()
-
-
-# Wire values of ``UiWindowSummary.window_kind``, written by the GUI window
-# providers and read by agent-side presentations.
-EMBEDDED_WINDOW_KIND = "embedded"
-MANAGED_WINDOW_KIND = "managed"
-DYNAMIC_SCOPE_WINDOW_KIND = "scope"
-QT_TOP_LEVEL_WINDOW_KIND = "qt_top_level"
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowSummary(UiWindowSemanticCarrier):
- schema_version: str
- identity: UiWindowIdentity
- title: str
- window_kind: str
- visible: bool
- focusable: bool
- manager_scope: UiWindowManagerScope | None = None
- error_dialog: bool = False
- """The window is a critical message box reporting an error."""
-
- @property
- def window_id(self) -> str:
- return self.identity.window_id
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowCatalog(AgentResultEnvelope, UiCatalogPageCarrier):
- windows: tuple[UiWindowSummary, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowOpenPolicy:
- """Policy for materializing a UI window before operating on it."""
-
- create_if_missing: bool = True
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWindowOperationRequest(UiWindowIdentity):
- """Shared request contract for operations against a UI window."""
-
- open_policy: UiWindowOpenPolicy
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWindowFocusRequest(UiWindowOperationRequest):
- @classmethod
- def from_fields(
- cls,
- *,
- window_id: str,
- create_if_missing: bool = True,
- ) -> "UiWindowFocusRequest":
- return cls(
- window_id=window_id,
- open_policy=UiWindowOpenPolicy(create_if_missing=create_if_missing),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowFocusResult(AgentResultEnvelope, UiWindowIdentity):
- focused: bool
- summary: UiWindowSummary | None = None
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWindowNavigateRequest(UiWindowOperationRequest):
- item_id: str | None = None
- field_path: str | None = None
-
- @property
- def has_target(self) -> bool:
- return self.item_id is not None or self.field_path is not None
-
- @classmethod
- def from_fields(
- cls,
- *,
- window_id: str,
- field_path: str | None = None,
- item_id: str | None = None,
- create_if_missing: bool = True,
- ) -> "UiWindowNavigateRequest":
- return cls(
- window_id=window_id,
- field_path=field_path,
- item_id=item_id,
- open_policy=UiWindowOpenPolicy(create_if_missing=create_if_missing),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowNavigateResult(AgentResultEnvelope, UiWindowIdentity):
- focused: bool
- navigated: bool
- created: bool
- summary: UiWindowSummary | None = None
- operation_id: str | None = None
- target_exposed: bool | None = None
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWindowCloseRequest(UiWindowIdentity):
- """Request a normal close for one currently open UI window."""
-
- @classmethod
- def from_fields(
- cls,
- *,
- window_id: str,
- ) -> "UiWindowCloseRequest":
- return cls(window_id=window_id)
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowCloseResult(AgentResultEnvelope, UiWindowIdentity):
- closed: bool
- summary: UiWindowSummary | None = None
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWindowSnapshotRequest(
- WindowSnapshotCaptureSpec,
- UiWindowOperationRequest,
- AgentCliRequest,
-):
- @classmethod
- def from_fields(
- cls,
- *,
- window_id: str,
- output_dir_path: str | None = None,
- capture_scope: str = WindowSnapshotCaptureScope.WIDGET.value,
- create_if_missing: bool = False,
- frame_condition: str = WindowSnapshotFrameCondition.IMMEDIATE.value,
- observation_timeout_s: float = WindowSnapshotCaptureSpec.observation_timeout_s,
- ) -> "UiWindowSnapshotRequest":
- if output_dir_path is None:
- output_dir_path = str(DEFAULT_AGENT_WINDOW_SNAPSHOT_DIR)
- return cls(
- window_id=window_id,
- output_dir_path=output_dir_path,
- capture_scope=WindowSnapshotCaptureScope(capture_scope),
- frame_condition=WindowSnapshotFrameCondition(frame_condition),
- observation_timeout_s=observation_timeout_s,
- open_policy=UiWindowOpenPolicy(create_if_missing=create_if_missing),
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "window_id": self.window_id,
- **to_jsonable(project_dataclass(WindowSnapshotCaptureSpec, self)),
- "create_if_missing": self.open_policy.create_if_missing,
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowSnapshotResult(
- WindowSnapshotCaptureSpec,
- AgentResultEnvelope,
- UiWindowIdentity,
-):
- captured: bool
- resource: AgentResourceRef | None = None
- summary: UiWindowSummary | None = None
- width: int | None = None
- height: int | None = None
- operation_id: str | None = None
- observation: WindowVisualObservation | None = None
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWidgetTreeRequest(
- WidgetTreeProjectionControls,
- UiWindowOperationRequest,
- AgentCliRequest,
-):
- actionable_only: bool = True
- include_tree: bool = False
- max_depth: int | None = 8
- max_nodes: int | None = 80
-
- @classmethod
- def from_fields(
- cls,
- *,
- window_id: str,
- create_if_missing: bool = False,
- maximum_text_length: int = (
- WidgetTreeProjectionControls.default_maximum_text_length()
- ),
- maximum_item_model_nodes: int | None = (
- WidgetTreeProjectionControls.default_maximum_item_model_nodes()
- ),
- truncation_suffix: str = (
- WidgetTreeProjectionControls.default_truncation_suffix()
- ),
- actionable_only: bool = True,
- include_tree: bool = False,
- max_depth: int | None = 8,
- max_nodes: int | None = 80,
- ) -> "UiWidgetTreeRequest":
- return cls(
- window_id=window_id,
- open_policy=UiWindowOpenPolicy(create_if_missing=create_if_missing),
- maximum_text_length=maximum_text_length,
- maximum_item_model_nodes=maximum_item_model_nodes,
- truncation_suffix=truncation_suffix,
- actionable_only=actionable_only,
- include_tree=include_tree,
- max_depth=max_depth,
- max_nodes=max_nodes,
- )
-
- @classmethod
- def default_include_tree(cls) -> bool:
- return False
-
- @classmethod
- def default_max_depth(cls) -> int:
- return 8
-
- @classmethod
- def default_max_nodes(cls) -> int:
- return 80
-
- def __post_init__(self) -> None:
- WidgetTreeProjectionControls.__post_init__(self)
- if self.max_depth is not None and self.max_depth < 0:
- raise ValueError("max_depth must be non-negative or None")
- if self.max_nodes is not None and self.max_nodes < 1:
- raise ValueError("max_nodes must be positive or None")
-
- def as_projection_policy(self) -> WidgetTreeProjectionPolicy:
- return WidgetTreeProjectionControls.as_projection_policy(
- self,
- maximum_depth=self.max_depth,
- maximum_nodes=self.max_nodes,
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "window_id": self.window_id,
- "create_if_missing": self.open_policy.create_if_missing,
- "maximum_text_length": self.maximum_text_length,
- "maximum_item_model_nodes": self.maximum_item_model_nodes,
- "truncation_suffix": self.truncation_suffix,
- "actionable_only": self.actionable_only,
- "include_tree": self.include_tree,
- "max_depth": self.max_depth,
- "max_nodes": self.max_nodes,
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiWidgetRect:
- x: int
- y: int
- width: int
- height: int
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateValuePreview:
- """Bounded display preview for an ObjectState field value."""
-
- type_name: str
- is_none: bool
- text: str
- truncated: bool = False
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiObjectStateFieldValuePreviewCarrier:
- """Shared raw/resolved value previews for ObjectState-aware UI DTOs."""
-
- raw_value_is_none: bool = False
- resolved_value_is_none: bool = False
- raw_value_preview: UiObjectStateValuePreview | None = field(
- default=None,
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=lambda owner, field_value: (
- field_value is not None or owner.raw_value_is_none
- )
- )
- },
- )
- resolved_value_preview: UiObjectStateValuePreview | None = field(
- default=None,
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=lambda owner, field_value: (
- field_value is not None or owner.resolved_value_is_none
- )
- )
- },
- )
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiObjectStateFieldSemanticCarrier(UiObjectStateFieldValuePreviewCarrier):
- """Shared ObjectState field semantics projected into agent-facing DTOs."""
-
- dirty: bool = False
- signature_diff: bool = False
- last_changed: bool = False
- semantic_markers: tuple[str, ...] = ()
- raw_value: JsonValue | None = None
- resolved_value: JsonValue | None = None
- inherited_value: bool = False
- provenance: "UiObjectStateFieldProvenance | None" = None
-
-
-@dataclass(frozen=True, kw_only=True)
-class UiWidgetActionSemanticCarrier(UiObjectStateFieldSemanticCarrier):
- """ObjectState and action-address semantics for one widget action."""
-
- action_role: str | None = None
- semantic_address: UiSemanticAddress | None = None
- object_state_scope_id: str | None = None
- field_path: str | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiWidgetTreeNode(WidgetNodeIdentity):
- visible: bool
- enabled: bool
- geometry: UiWidgetRect
- global_geometry: UiWidgetRect
- tool_tip: str
- status_tip: str
- whats_this: str
- window_title: str
- text: str | None
- text_truncated: bool
- title: str | None
- action_kinds: tuple[str, ...]
- clickable: bool
- actionable: bool
- checkable: bool | None
- checked: bool | None
- current_index: int | None
- current_text: str | None
- item_count: int | None
- children: tuple["UiWidgetTreeNode", ...]
- item_texts: tuple[str, ...] = ()
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWidgetActionSummary(UiWidgetActionSemanticCarrier, WidgetNodeIdentity):
- label: str | None = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- visible: bool = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- enabled: bool = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- geometry: UiWidgetRect = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- global_geometry: UiWidgetRect = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- action_kinds: tuple[str, ...] = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- clickable: bool = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=always_project_compact_field
- )
- }
- )
- checkable: bool | None
- checked: bool | None = field(
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=lambda owner, field_value: (
- isinstance(field_value, bool)
- and (field_value or owner.checkable is True)
- )
- )
- }
- )
- current_index: int | None
- current_text: str | None
- item_count: int | None
- tool_tip: str
- item_texts: tuple[str, ...] = ()
- context_label: str | None = None
- raw_value: JsonValue | None = field(
- default=None,
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=lambda owner, field_value: (
- owner.raw_value_preview is None
- and (field_value is not None or owner.raw_value_is_none)
- )
- )
- },
- )
- resolved_value: JsonValue | None = field(
- default=None,
- metadata={
- COMPACT_FIELD_PROJECTION_METADATA_KEY: CompactFieldProjection(
- includes=lambda owner, field_value: (
- owner.resolved_value_preview is None
- and (field_value is not None or owner.resolved_value_is_none)
- )
- )
- },
- )
-
- def compact_projection(self) -> JsonObject:
- """Return the field-declaration-owned compact action projection."""
-
- return {
- field_name: to_jsonable(field_value)
- for field_name, field_value in compact_dataclass_projection(self).items()
- }
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiWidgetActionInvokeRequest(
- UiWindowOperationRequest,
- UiMutationRequestTokenCarrier,
- AgentCliRequest,
-):
- path_id: str
- action_kind: str = "auto"
- target_index: int | None = None
-
- @classmethod
- def from_fields(
- cls,
- *,
- window_id: str,
- path_id: str,
- action_kind: str = "button",
- target_index: int | None = None,
- create_if_missing: bool = False,
- request_token: str | None = None,
- ) -> "UiWidgetActionInvokeRequest":
- return cls(
- window_id=window_id,
- open_policy=UiWindowOpenPolicy(create_if_missing=create_if_missing),
- path_id=path_id,
- action_kind=action_kind,
- target_index=target_index,
- request_token=UiMutationRequestToken(value=request_token),
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "window_id": self.window_id,
- "path_id": self.path_id,
- "action_kind": self.action_kind,
- "target_index": self.target_index,
- "create_if_missing": self.open_policy.create_if_missing,
- "request_token": self.request_token.value,
- }
-
-
-class UiWidgetActionIssueCode(str, Enum):
- """Stable issue identities for projected widget action invocation."""
-
- RESOLUTION_FAILED = "ui_widget_action_resolution_failed"
- WIDGET_UNKNOWN = "unknown_ui_widget"
- ACTION_UNSUPPORTED = "ui_widget_action_unsupported"
- INDEX_INVALID = "ui_widget_index_invalid"
- NOT_VISIBLE = "ui_widget_not_visible"
- DISABLED = "ui_widget_disabled"
- NOT_CLICKABLE = "ui_widget_not_clickable"
- ACTION_KIND_UNAVAILABLE = "ui_widget_action_kind_unavailable"
- OPERATION_OUTCOME_INVALID = "ui_widget_action_operation_outcome_invalid"
-
-
-@dataclass(frozen=True, slots=True)
-class UiWidgetActionInvokeResult(
- AgentResultEnvelope,
- UiWindowIdentity,
-):
- path_id: str
- action_kind: str
- invoked: bool
- receipt: UiMutationReceipt
- summary: UiWidgetActionSummary | None = None
-
- @property
- def outcome(self) -> UiWidgetActionInvocationOutcome:
- """Project the terminal operation outcome from the invocation fact."""
-
- return UiWidgetActionInvocationOutcome.from_invoked(self.invoked)
-
- def resolve_operation(
- self,
- operation: "UiBridgeOperationRef",
- ) -> "UiWidgetActionInvokeResult":
- """Resolve an accepted placeholder from its authoritative operation."""
-
- try:
- status = UiBridgeOperationStatus.named(operation.status)
- except ValueError:
- status = OperationUnavailable
- try:
- outcome = (
- UiWidgetActionInvocationOutcome(operation.outcome)
- if operation.outcome is not None
- else None
- )
- except ValueError:
- outcome = None
- errors = operation.errors
- if status is OperationCompleted and outcome is None:
- errors = (
- *errors,
- AgentError(
- code=UiWidgetActionIssueCode.OPERATION_OUTCOME_INVALID.value,
- message=(
- "The widget action operation completed without a declared "
- f"terminal outcome: {operation.outcome!r}."
- ),
- ),
- )
- invoked = (
- outcome.invoked
- if status is OperationCompleted and outcome is not None
- else False
- )
- return replace(
- self,
- invoked=invoked,
- errors=errors,
- warnings=operation.warnings,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiWidgetTreeResult(AgentResultEnvelope, UiWindowIdentity):
- projected: bool
- root: UiWidgetTreeNode | None = None
- actionable_widgets: tuple[UiWidgetActionSummary, ...] = ()
- summary: UiWindowSummary | None = None
- widget_count: int = 0
- actionable_count: int = 0
- returned_widget_count: int = 0
- returned_actionable_count: int = 0
- tree_truncated: bool = False
- actionable_widgets_truncated: bool = False
- actionable_only: bool = True
- include_tree: bool = False
- max_depth: int | None = None
- max_nodes: int | None = None
-
- def as_jsonable(self, *, compact_actions: bool = True) -> JsonObject:
- """Serialize the typed result with action compaction owned by its DTO."""
-
- payload = to_jsonable(self)
- if not isinstance(payload, dict):
- raise TypeError("Widget tree serialization did not produce an object.")
- if compact_actions:
- payload["actionable_widgets"] = [
- action.compact_projection() for action in self.actionable_widgets
- ]
- return payload
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldProvenance:
- """Resolved source address for an inherited ObjectState field."""
-
- source_scope_id: str | None
- source_type: str | None
- source_field_path: str | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldSummary(UiObjectStateFieldSemanticCarrier):
- """Field-level ObjectState semantics and bounded raw/resolved values."""
-
- schema_version: str
- address: UiSemanticAddress
- field_name: str
- container_path: str
- object_state_path_type: str
- raw_value_type: str
- resolved_value_type: str | None
- parameter_description: str | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldPathIndex:
- """Field-path relationship index for ObjectState field projections."""
-
- fields: tuple[UiObjectStateFieldSummary, ...]
-
- @property
- def container_field_paths(self) -> frozenset[str]:
- field_paths = tuple(field.address.field_path for field in self.fields)
- return frozenset(
- field_path
- for field_path in field_paths
- if any(
- DottedFieldPath(field_path).contains_path(other_path)
- for other_path in field_paths
- if other_path != field_path
- )
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldHelpRequest(UiSemanticAddress):
- """Request docs/help for one live ObjectState field address."""
-
- max_description_chars: int = 4_000
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldHelpQuery(AgentCliRequest):
- """Public query for field help; scope may be inferred when unique."""
-
- field_path: str
- object_state_scope_id: str | None = None
- window_id: str | None = None
- max_description_chars: int = 4_000
-
- @classmethod
- def from_fields(
- cls,
- *,
- field_path: str,
- object_state_scope_id: str | None = None,
- window_id: str | None = None,
- max_description_chars: int = 4_000,
- ) -> "UiObjectStateFieldHelpQuery":
- return cls(
- field_path=field_path,
- object_state_scope_id=object_state_scope_id,
- window_id=window_id,
- max_description_chars=max_description_chars,
- )
-
- def __post_init__(self) -> None:
- if self.max_description_chars < 0:
- raise ValueError("max_description_chars must be nonnegative.")
-
- def concrete_request(
- self,
- object_state_scope_id: str,
- ) -> UiObjectStateFieldHelpRequest:
- return UiObjectStateFieldHelpRequest(
- object_state_scope_id=object_state_scope_id,
- field_path=self.field_path,
- window_id=self.window_id,
- max_description_chars=self.max_description_chars,
- )
-
- def error_address(self) -> UiSemanticAddress:
- return UiSemanticAddress(
- object_state_scope_id=self.object_state_scope_id or "",
- field_path=self.field_path,
- window_id=self.window_id,
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "field_path": self.field_path,
- "object_state_scope_id": self.object_state_scope_id,
- "window_id": self.window_id,
- "max_description_chars": self.max_description_chars,
- }
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiObjectStateFieldHelpResult(AgentResultEnvelope):
- """Display-ready help for one ObjectState field from Python introspection."""
-
- address: UiSemanticAddress
- field: UiObjectStateFieldSummary | None = None
- object_type: str | None = None
- help_target_type: str | None = None
- parameter_name: str | None = None
- target_summary: str | None = None
- target_description: str | None = None
- summary: str | None = None
- description: str | None = None
- enum_values: tuple[str, ...] = ()
- description_truncated: bool = False
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiObjectStateFieldMutationRequest(
- UiMutationRequestTokenCarrier,
- UiSemanticAddress,
- AgentCliRequest,
-):
- """Request an ObjectState-owned field update or reset."""
-
- value: JsonValue | None = None
- reset: bool = False
- include_field_values: bool = True
-
- @classmethod
- def from_fields(
- cls,
- *,
- object_state_scope_id: str,
- field_path: str,
- value: dict | list | str | int | float | bool | None = None,
- reset: bool = False,
- window_id: str | None = None,
- include_field_values: bool = True,
- request_token: str | None = None,
- ) -> "UiObjectStateFieldMutationRequest":
- return cls(
- object_state_scope_id=object_state_scope_id,
- field_path=field_path,
- window_id=window_id,
- value=value,
- reset=reset,
- include_field_values=include_field_values,
- request_token=UiMutationRequestToken(value=request_token),
- )
-
- def __post_init__(self) -> None:
- if self.reset and self.value is not None:
- raise ValueError("reset=True cannot be combined with value.")
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "object_state_scope_id": self.object_state_scope_id,
- "field_path": self.field_path,
- "value": self.value,
- "reset": self.reset,
- "window_id": self.window_id,
- "include_field_values": self.include_field_values,
- "request_token": self.request_token.value,
- }
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiObjectStateFieldMutationResult(AgentResultEnvelope):
- """Before/after projection for one ObjectState-owned field mutation."""
-
- address: UiSemanticAddress
- mutated: bool
- reset: bool
- receipt: UiMutationReceipt
- before: UiObjectStateFieldSummary | None = None
- after: UiObjectStateFieldSummary | None = None
- current_snapshot: "UiSnapshotRef | None" = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateScopeSummary(UiFieldCatalogPageCarrier):
- schema_version: str
- identity: UiObjectStateScopeIdentity
- object_type: str
- parameter_count: int
- dirty_field_count: int
- signature_diff_field_count: int
- has_unsaved_changes: bool = False
- has_default_overrides: bool = False
- dirty_marker: str = "*"
- signature_diff_marker: str = "_"
- last_changed_field: str | None = None
- registered: bool = True
- fields: tuple[UiObjectStateFieldSummary, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateScopeCatalog(
- AgentResultEnvelope,
- UiTimeTravelRuntimeState,
- UiCatalogPageCarrier,
-):
- object_state_token: int
- current_branch: str
- current_snapshot_index: int
- scopes: tuple[UiObjectStateScopeSummary, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateScopeListRequest(
- UiObjectStateScopeVisibility,
- AgentCliRequest,
-):
- scope_ids: tuple[str, ...] = ()
- include_fields: bool = False
- field_limit: int = 200
- field_offset: int = 0
- include_field_values: bool = False
- include_field_descriptions: bool = False
- field_paths: tuple[str, ...] = ()
- field_filter: type[UiObjectStateFieldFilter] = AllFields
-
- @classmethod
- def from_visibility_options(
- cls,
- visibility: UiObjectStateScopeVisibility,
- field_options: UiObjectStateFieldListOptions,
- *,
- scope_ids: tuple[str, ...] = (),
- ) -> "UiObjectStateScopeListRequest":
- return cls(
- scope_ids=scope_ids,
- include_system_scopes=visibility.include_system_scopes,
- include_fields=field_options.include_fields,
- field_limit=field_options.field_limit,
- field_offset=field_options.field_offset,
- include_field_values=field_options.include_field_values,
- include_field_descriptions=field_options.include_field_descriptions,
- field_paths=field_options.field_paths,
- field_filter=field_options.field_filter,
- )
-
- @classmethod
- def from_fields(
- cls,
- *,
- scope_ids: tuple[str, ...] = (),
- include_system_scopes: bool = False,
- include_fields: bool = False,
- include_field_values: bool = False,
- field_filter: str = AllFields.name,
- field_limit: int = 200,
- field_offset: int = 0,
- ) -> "UiObjectStateScopeListRequest":
- return cls.from_visibility_options(
- UiObjectStateScopeVisibility(
- include_system_scopes=include_system_scopes,
- ),
- UiObjectStateFieldListOptions(
- include_fields=include_fields,
- include_field_values=include_field_values,
- field_limit=field_limit,
- field_offset=field_offset,
- field_filter=UiObjectStateFieldFilter.named(field_filter),
- ),
- scope_ids=tuple(scope_ids),
- )
-
- def filtered_catalog(
- self,
- catalog: UiObjectStateScopeCatalog,
- ) -> UiObjectStateScopeCatalog:
- if not self.scope_ids:
- return catalog
- requested_scope_ids = set(self.scope_ids)
- return replace(
- catalog,
- scopes=tuple(
- scope
- for scope in catalog.scopes
- if scope.identity.object_state_scope_id in requested_scope_ids
- ),
- )
-
- def as_tool_arguments(self) -> JsonObject:
- return {
- "scope_ids": list(self.scope_ids),
- "include_system_scopes": self.include_system_scopes,
- "include_fields": self.include_fields,
- "include_field_values": self.include_field_values,
- "field_filter": self.field_filter.name,
- "field_limit": self.field_limit,
- "field_offset": self.field_offset,
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldProjection(UiObjectStateFieldSemanticCarrier):
- """Compact ObjectState field row for agent-facing field-list queries."""
-
- field_path: str
- field_name: str
- container_path: str
- object_state_path_type: str
- raw_value_type: str
- resolved_value_type: str | None
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldScopeProjection:
- """Compact ObjectState scope row for field-list query results."""
-
- scope_id: str
- object_type: str
- dirty_field_count: int
- signature_diff_field_count: int
- has_unsaved_changes: bool
- has_default_overrides: bool
- fields: tuple[UiObjectStateFieldProjection, ...]
-
-
-@dataclass(frozen=True, slots=True, kw_only=True)
-class UiObjectStateFieldListResult(AgentResultEnvelope):
- """Filtered, paged ObjectState field projection for agent tools."""
-
- object_state_token: int
- current_branch: str
- current_snapshot_index: int
- requested_scope_ids: tuple[str, ...]
- field_paths: tuple[str, ...]
- field_path_contains: tuple[str, ...]
- field_filter: type[UiObjectStateFieldFilter]
- include_container_fields: bool
- matched_scope_count: int
- matched_field_count: int
- returned_field_count: int
- field_limit: int
- field_offset: int
- next_offset: int | None
- truncated: bool
- scopes: tuple[UiObjectStateFieldScopeProjection, ...]
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateFieldListQuery(AgentCliRequest):
- """Public ObjectState field query projected from the scope-list bridge ABI."""
-
- source_query_scan_limit: ClassVar[int] = 1_000
-
- scope_ids: tuple[str, ...] = ()
- field_paths: tuple[str, ...] = ()
- field_path_contains: tuple[str, ...] = ()
- include_system_scopes: bool = False
- include_clean_fields: bool = True
- include_container_fields: bool = False
- field_filter: type[UiObjectStateFieldFilter] = AllFields
- include_field_values: bool = False
- field_limit: int = 200
- field_offset: int = 0
- max_fields: int = 100
- max_value_items: int = 20
- max_value_chars: int = 1000
-
- @classmethod
- def from_fields(
- cls,
- *,
- scope_ids: tuple[str, ...] = (),
- field_paths: tuple[str, ...] = (),
- field_path_contains: tuple[str, ...] = (),
- include_system_scopes: bool = False,
- include_clean_fields: bool = True,
- include_container_fields: bool = False,
- field_filter: str = AllFields.name,
- include_field_values: bool = False,
- field_limit: int = 200,
- field_offset: int = 0,
- max_fields: int = 100,
- max_value_items: int = 20,
- max_value_chars: int = 1000,
- ) -> "UiObjectStateFieldListQuery":
- return cls(
- scope_ids=tuple(scope_ids),
- field_paths=tuple(field_paths),
- field_path_contains=tuple(field_path_contains),
- include_system_scopes=include_system_scopes,
- include_clean_fields=include_clean_fields,
- include_container_fields=include_container_fields,
- field_filter=UiObjectStateFieldFilter.named(field_filter),
- include_field_values=include_field_values,
- field_limit=field_limit,
- field_offset=field_offset,
- max_fields=max_fields,
- max_value_items=max_value_items,
- max_value_chars=max_value_chars,
- )
-
- def __post_init__(self) -> None:
- if self.field_limit < 0 or self.field_offset < 0:
- raise ValueError("field_limit and field_offset must be nonnegative.")
- if self.max_fields < 0 or self.max_value_items < 0 or self.max_value_chars < 0:
- raise ValueError(
- "max_fields, max_value_items, and max_value_chars must be nonnegative."
- )
-
- @property
- def contains_terms(self) -> tuple[str, ...]:
- return tuple(term.lower() for term in self.field_path_contains)
-
- @property
- def exact_source_field_paths(self) -> tuple[str, ...]:
- if (
- self.field_paths
- and not self.contains_terms
- and not self.include_container_fields
- ):
- return self.field_paths
- return ()
-
- @property
- def source_field_limit(self) -> int:
- if self.exact_source_field_paths:
- return max(1, len(self.exact_source_field_paths))
- return max(
- self.source_query_scan_limit,
- self.field_limit + self.field_offset,
- self.max_fields,
- )
-
- @property
- def return_limit(self) -> int:
- if self.max_fields:
- return min(self.field_limit, self.max_fields)
- return self.field_limit
-
- def scope_list_request(self) -> UiObjectStateScopeListRequest:
- return UiObjectStateScopeListRequest.from_visibility_options(
- UiObjectStateScopeVisibility(
- include_system_scopes=self.include_system_scopes,
- ),
- UiObjectStateFieldListOptions(
- include_fields=True,
- include_field_values=self.include_field_values,
- field_limit=self.source_field_limit,
- field_offset=0,
- field_paths=self.exact_source_field_paths,
- field_filter=self.field_filter,
- ),
- scope_ids=self.scope_ids,
- )
+@dataclass(frozen=True, slots=True)
+class UiLiveMeasurementsState(
+ UiStateSurfaceEnvelope,
+ UiCodeDocumentCurrentRevision,
+ UiCurrentSnapshotState,
+ SelectedScopeIdsCarrier,
+):
+ """Bounded live measurement tables retained by the running Plate Manager."""
- def as_tool_arguments(self) -> JsonObject:
- return {
- "scope_ids": list(self.scope_ids),
- "field_paths": list(self.field_paths),
- "field_path_contains": list(self.field_path_contains),
- "include_system_scopes": self.include_system_scopes,
- "include_clean_fields": self.include_clean_fields,
- "include_container_fields": self.include_container_fields,
- "field_filter": self.field_filter.name,
- "include_field_values": self.include_field_values,
- "field_limit": self.field_limit,
- "field_offset": self.field_offset,
- "max_fields": self.max_fields,
- "max_value_items": self.max_value_items,
- "max_value_chars": self.max_value_chars,
- }
+ object_state_token: int
+ retained_entry_count: int
+ visible_entry_count: int
+ total_row_count: int
+ latest_sequence_id: int | None
+ entries: tuple[UiLiveMeasurementEntryState, ...]
@dataclass(frozen=True, slots=True)
-class UiCodeDocumentValidationRequest(
- UiCodeDocumentIdentity,
- UiCodeDocumentOptionalBaseRevision,
- AgentCliRequest,
+class UiPipelineEditorState(
+ UiStateSurfaceEnvelope,
+ UiCodeDocumentCurrentRevision,
+ UiCurrentSnapshotState,
+ SelectedScopeIdsCarrier,
):
- source: str
+ object_state_token: int
+ current_plate_scope_id: str | None
+ pipeline_scope_id: str | None
+ steps: tuple[PipelineStepState, ...]
- @classmethod
- def from_fields(
- cls,
- *,
- document_id: str,
- source: str,
- base_revision_token: str | None = None,
- ) -> "UiCodeDocumentValidationRequest":
- return cls(
- document_id=document_id,
- source=source,
- base_revision_token=base_revision_token,
- )
- def as_tool_arguments(self) -> JsonObject:
- return {
- "document_id": self.document_id,
- "source": self.source,
- "base_revision_token": self.base_revision_token,
- }
+@dataclass(frozen=True, slots=True)
+class UiDebugActionState(SelectedScopeIdsCarrier):
+ action_id: str
+ label: str
+ placement: str
+ enabled: bool
+ side_effects: tuple[str, ...]
+ confirmation_required: bool
+ requires_active_debug_session: bool
+ disabled_error: AgentError | None = None
@dataclass(frozen=True, slots=True)
-class UiCodeDocumentValidationResult(UiCodeDocumentIdentity):
- schema_version: str
- valid: bool
- normalized_scope_ids: tuple[str, ...] = ()
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
+class UiDebugCursorState:
+ step_index: int
+ step_scope_id: str | None
+ group_key: str | None
+ invocation_key: str | None
+ pattern_group_identity: str | None
+ dirty: bool = False
-@dataclass(frozen=True, kw_only=True)
-class UiBridgeConfirmationPolicy(UiBridgeConfirmationRequirementCarrier):
- """Shared mutation confirmation policy for UI bridge requests."""
+@dataclass(frozen=True, slots=True)
+class UiDebugTerminalSummaryState:
+ debug_session_id: str
+ plate_scope_id: str
+ terminal_status: str
+ command_type: str | None
+ axis_id: str | None
+ snapshot_id: str | None
+ snapshot_store_ref: str | None
+ snapshot_store_backend: str | None
+ step_name: str | None
+ callable_name: str | None
+ cursor: UiDebugCursorState | None
+ completed_at_unix: float | None
+
+@dataclass(frozen=True)
+class UiDebugSessionIdentityCarrier:
+ debug_session_id: str
-@dataclass(frozen=True, kw_only=True)
-class UiBridgeBranchMutationPolicy(UiBridgeConfirmationPolicy):
- """Shared branch creation policy for ObjectState movement requests."""
- allow_auto_branch: bool = False
+@dataclass(frozen=True)
+class UiDebugSnapshotStoreRefCarrier:
+ snapshot_store_ref: str | None
+ snapshot_store_backend: str | None
@dataclass(frozen=True, slots=True)
-class UiCodeDocumentApplyRequest(
- UiCodeDocumentIdentity,
- UiCodeDocumentBaseRevision,
- SelectedScopeIdsCarrier,
- UiBridgeConfirmationPolicy,
- UiMutationRequestTokenCarrier,
- AgentCliRequest,
-):
- source: str
- snapshot_label: str | None = None
- apply_if_time_traveling: bool = False
+class UiProgressIdentityState:
+ execution_id: str
+ subject_id: str
+ axis_id: str
+ step_name: str
- @classmethod
- def from_fields(
- cls,
- *,
- document_id: str,
- source: str,
- base_revision_token: str,
- selection_mode: str = SelectedItems.name,
- selected_scope_ids: tuple[str, ...] = (),
- require_confirmation: bool = True,
- snapshot_label: str | None = None,
- apply_if_time_traveling: bool = False,
- request_token: str | None = None,
- ) -> "UiCodeDocumentApplyRequest":
- return cls(
- document_id=document_id,
- source=source,
- base_revision_token=base_revision_token,
- selection_mode=selection_mode,
- selected_scope_ids=tuple(selected_scope_ids),
- confirmation_requirement=UiBridgeConfirmationRequirement.from_flag(
- require_confirmation
- ),
- request_token=UiMutationRequestToken(value=request_token),
- snapshot_label=snapshot_label,
- apply_if_time_traveling=apply_if_time_traveling,
- )
- def as_tool_arguments(self) -> JsonObject:
- return {
- "document_id": self.document_id,
- "source": self.source,
- "base_revision_token": self.base_revision_token,
- "selection_mode": self.selection_mode,
- "selected_scope_ids": list(self.selected_scope_ids),
- "require_confirmation": self.confirmation_is_required(),
- "snapshot_label": self.snapshot_label,
- "apply_if_time_traveling": self.apply_if_time_traveling,
- "request_token": self.request_token.value,
- }
+@dataclass(frozen=True, slots=True)
+class UiDebugRuntimeFrameState(
+ UiDebugSessionIdentityCarrier,
+ UiDebugSnapshotStoreRefCarrier,
+):
+ progress_identity: UiProgressIdentityState
+ cursor: UiDebugCursorState
+ event_type: str
+ step_name: str
+ callable_name: str | None
+ snapshot_id: str | None
+ timestamp: float
@dataclass(frozen=True, slots=True)
-class UiCodeDocumentApplyResult(UiCodeDocumentIdentity):
- schema_version: str
- applied: bool
- base_revision_token: str
- receipt: UiMutationReceipt
- outcome: str = "not_applied"
- operation_id: str | None = None
- new_revision_token: str | None = None
- current_revision_token: str | None = None
- current_snapshot: UiSnapshotRef | None = None
- undo_snapshot: UiSnapshotRef | None = None
- pre_apply_snapshot: UiSnapshotRef | None = None
- post_apply_snapshot: UiSnapshotRef | None = None
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
+class UiPipelineDebugSessionState(
+ UiStateSurfaceEnvelope,
+ UiCodeDocumentCurrentRevision,
+ UiCurrentSnapshotState,
+ SelectedScopeIdsCarrier,
+):
+ object_state_token: int
+ current_plate_scope_id: str | None
+ pipeline_scope_id: str | None
+ manager_execution_state: str
+ initialized: bool
+ compiled: bool
+ phase: str
+ active_session_id: str | None
+ execution_id: str | None
+ axis_id: str | None
+ selected_source_group: str | None
+ snapshot_store_ref: str | None
+ snapshot_store_backend: str | None
+ terminal_status: str | None
+ cursor: UiDebugCursorState | None
+ terminal_summary: UiDebugTerminalSummaryState | None
+ actions: tuple[UiDebugActionState, ...]
+ current_frame: UiDebugRuntimeFrameState | None = None
+ last_frame: UiDebugRuntimeFrameState | None = None
@dataclass(frozen=True, slots=True)
@@ -2599,246 +270,6 @@ class UiSelectedPlateWorkflowResult:
schema_version: str
workflow: UiSelectedPlateWorkflowKind
action_result: UiActionInvokeResult
- state_surface_id: str = PlateManagerStateSurfaceIdentityDeclaration.require_value()
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiSnapshotCatalog(UiTimeTravelRuntimeState):
- schema_version: str
- current_branch: str
- current_snapshot_index: int
- object_state_token: int
- snapshots: tuple[UiSnapshotRef, ...]
- branches: tuple[UiBranchRef, ...]
- warnings: tuple[AgentWarning, ...] = ()
- errors: tuple[AgentError, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiSnapshotListRequest(UiObjectStateScopeVisibility):
- @classmethod
- def from_fields(
- cls,
- *,
- scope_visibility: dict | None = None,
- ) -> "UiSnapshotListRequest":
- visibility = UiObjectStateScopeVisibility.from_mapping(scope_visibility)
- return cls(include_system_scopes=visibility.include_system_scopes)
-
-
-@dataclass(frozen=True, slots=True)
-class UiSnapshotRestoreRequest(
- UiSnapshotIdentity,
- UiBridgeBranchMutationPolicy,
- UiObjectStateScopeVisibility,
-):
- index: int | None = None
- branch: str | None = None
-
- @classmethod
- def from_fields(
- cls,
- *,
- snapshot_id: str | None = None,
- index: int | None = None,
- branch: str | None = None,
- scope_visibility: dict | None = None,
- require_confirmation: bool = True,
- allow_auto_branch: bool = False,
- ) -> "UiSnapshotRestoreRequest":
- visibility = UiObjectStateScopeVisibility.from_mapping(scope_visibility)
- return cls(
- snapshot_id=snapshot_id,
- index=index,
- branch=branch,
- include_system_scopes=visibility.include_system_scopes,
- confirmation_requirement=UiBridgeConfirmationRequirement.from_flag(
- require_confirmation
- ),
- allow_auto_branch=allow_auto_branch,
- )
-
- @property
- def operation_target_id(self) -> str | None:
- if self.snapshot_id is not None:
- return self.snapshot_id
- if self.branch is not None:
- return self.branch
- if self.index is not None:
- return str(self.index)
- return None
-
-
-@dataclass(frozen=True, slots=True)
-class UiTimeTravelHeadRequest(UiBridgeConfirmationPolicy):
- @classmethod
- def from_fields(
- cls,
- *,
- require_confirmation: bool = True,
- ) -> "UiTimeTravelHeadRequest":
- return cls(
- confirmation_requirement=UiBridgeConfirmationRequirement.from_flag(
- require_confirmation
- )
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiSnapshotRestoreResult(UiCurrentSnapshotState):
- schema_version: str
- restored: bool
- target_snapshot: UiSnapshotRef | None
- catalog: UiSnapshotCatalog | None = None
- operation_id: str | None = None
- receipt: UiMutationReceipt | None = None
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiBranchCatalog:
- schema_version: str
- current_branch: str
- branches: tuple[UiBranchRef, ...]
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class UiBranchSwitchRequest(UiBridgeBranchMutationPolicy):
- branch: str
-
- @classmethod
- def from_fields(
- cls,
- *,
- branch: str,
- require_confirmation: bool = True,
- allow_auto_branch: bool = False,
- ) -> "UiBranchSwitchRequest":
- return cls(
- branch=branch,
- confirmation_requirement=UiBridgeConfirmationRequirement.from_flag(
- require_confirmation
- ),
- allow_auto_branch=allow_auto_branch,
- )
-
- def as_restore_request(self) -> UiSnapshotRestoreRequest:
- return UiSnapshotRestoreRequest(
- branch=self.branch,
- confirmation_requirement=self.confirmation_requirement,
- allow_auto_branch=self.allow_auto_branch,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeOperationRoute:
- operation_name: str
- request_id: str | None = None
- target_id: str | None = None
-
-
-UNKNOWN_UI_BRIDGE_OPERATION_ROUTE = UiBridgeOperationRoute(
- operation_name=UI_BRIDGE_UNKNOWN_OPERATION,
-)
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeOperationIdentity:
- operation_id: str
- route: UiBridgeOperationRoute
-
- @property
- def operation_name(self) -> str:
- return self.route.operation_name
-
- @property
- def request_id(self) -> str | None:
- return self.route.request_id
-
- @property
- def target_id(self) -> str | None:
- return self.route.target_id
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeOperationStatusRequest(AgentDataclassCliRequest):
- operation_id: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeOperationWaitRequest(AgentCliRequest):
- """Bounded terminal wait for one accepted UI bridge operation."""
-
- operation_id: str
- timeout_seconds: float = 30.0
- poll_interval_seconds: float = 0.5
-
- @classmethod
- def from_fields(
- cls,
- operation_id: str,
- timeout_seconds: float = 30.0,
- poll_interval_seconds: float = 0.5,
- ) -> "UiBridgeOperationWaitRequest":
- return cls(
- operation_id=operation_id,
- timeout_seconds=timeout_seconds,
- poll_interval_seconds=poll_interval_seconds,
- )
-
- def __post_init__(self) -> None:
- if not self.operation_id:
- raise ValueError("operation_id must not be empty.")
- if (
- not isfinite(self.timeout_seconds)
- or not 0.0 <= self.timeout_seconds <= 120.0
- ):
- raise ValueError("timeout_seconds must be between 0 and 120.")
- if (
- not isfinite(self.poll_interval_seconds)
- or not 0.05 <= self.poll_interval_seconds <= 5.0
- ):
- raise ValueError("poll_interval_seconds must be between 0.05 and 5.")
-
- def as_tool_arguments(self) -> JsonObject:
- """Project the declared operation-wait factory fields to MCP arguments."""
-
- return {
- "operation_id": self.operation_id,
- "timeout_seconds": self.timeout_seconds,
- "poll_interval_seconds": self.poll_interval_seconds,
- }
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeOperationRef(AgentTimedStatusEnvelope):
- identity: UiBridgeOperationIdentity
- completed_at_unix: float | None = None
- outcome: str | None = None
- result_payload: JsonObject | None = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeEnvelopeBase(UiBridgeApplicationIdentity):
- schema_version: str
- bridge_protocol_version: str
- request_id: str
- payload: JsonObject
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeRequestEnvelope(UiBridgeEnvelopeBase):
- operation: str
- auth_token: str | None
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeResponseEnvelope(UiBridgeEnvelopeBase):
- ok: bool
+ state_surface_id: str = DatasetListStateSurfaceIdentity.require_value()
errors: tuple[AgentError, ...] = ()
warnings: tuple[AgentWarning, ...] = ()
diff --git a/openhcs/agent/dto/viewer.py b/openhcs/agent/dto/viewer.py
index 91e4f38581..f88b7e9de6 100644
--- a/openhcs/agent/dto/viewer.py
+++ b/openhcs/agent/dto/viewer.py
@@ -11,7 +11,7 @@
from metaclass_registry import AutoRegisterMeta
from polystore.streaming.identity import StreamProducerIdentity
from polystore.streaming_constants import StreamingDataType
-from pyqt_reactive.services.window_snapshot import (
+from formbridge.services.window_snapshot import (
WindowSnapshotCaptureScope,
WindowSnapshotCaptureSpec,
WindowSnapshotFrameCondition,
@@ -33,8 +33,8 @@
ViewerNativeViewportPresentation,
)
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import (
AgentCliArgumentSpec,
AgentCliRequest,
AgentError,
diff --git a/openhcs/agent/exceptions.py b/openhcs/agent/exceptions.py
index 76fb676373..fb2078d483 100644
--- a/openhcs/agent/exceptions.py
+++ b/openhcs/agent/exceptions.py
@@ -4,7 +4,7 @@
from typing import ClassVar
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
class AgentFacingErrorMixin:
diff --git a/openhcs/agent/path_policy.py b/openhcs/agent/path_policy.py
index 4be39cebf3..0146803db8 100644
--- a/openhcs/agent/path_policy.py
+++ b/openhcs/agent/path_policy.py
@@ -8,7 +8,8 @@
from pathlib import Path
from openhcs.agent.exceptions import AgentFacingErrorMixin
-from openhcs.agent.runtime_platform import AgentRuntimePlatform
+from formbridge.bridge.service import UiBridgeOutputPathPolicy, UiBridgeRequestRejectedError
+from formbridge.runtime_platform import AgentRuntimePlatform
class AgentPathLocationAuthority:
@@ -90,7 +91,7 @@ def display(self) -> str:
@dataclass(frozen=True, slots=True)
-class AgentPathPolicy:
+class AgentPathPolicy(UiBridgeOutputPathPolicy):
"""Own the roots and environment contract for agent-visible paths."""
readable_roots_environment_key = "OPENHCS_AGENT_READ_ROOTS"
@@ -189,6 +190,12 @@ def assert_writable(self, path: str | Path) -> Path:
)
return candidate
+ def writable_directory(self, path: str | Path) -> Path:
+ try:
+ return self.assert_writable(path)
+ except AgentPathPolicyError as exc:
+ raise UiBridgeRequestRejectedError((exc.to_agent_error(),)) from exc
+
class AgentPathPolicyError(AgentFacingErrorMixin, ValueError):
"""Raised when a path is outside the agent policy boundary."""
diff --git a/openhcs/agent/runtime_platform.py b/openhcs/agent/runtime_platform.py
deleted file mode 100644
index b2386664e9..0000000000
--- a/openhcs/agent/runtime_platform.py
+++ /dev/null
@@ -1,364 +0,0 @@
-"""Host-platform families for headless agent runtime paths."""
-
-from __future__ import annotations
-
-import os
-import sys
-import tempfile
-from abc import ABC
-from collections.abc import Mapping
-from pathlib import Path
-from typing import ClassVar
-
-from metaclass_registry import AutoRegisterMeta
-
-
-class AgentRuntimePlatform(ABC, metaclass=AutoRegisterMeta):
- """Filesystem, process and session semantics of one host platform.
-
- Each platform is a subclass; ``name`` is its spelling in reports.
- """
-
- __registry_key__ = "name"
- __skip_if_no_key__ = True
-
- name: ClassVar[str | None] = None
-
- @staticmethod
- def current_type() -> type[AgentRuntimePlatform]:
- """Return the platform class of the running interpreter."""
- if sys.platform.startswith("win"):
- return WindowsRuntimePlatform
- if sys.platform == "darwin":
- return MacOSRuntimePlatform
- if sys.platform.startswith("linux"):
- return LinuxRuntimePlatform
- if os.name == "posix":
- return PosixRuntimePlatform
- return OtherRuntimePlatform
-
- @classmethod
- def current(cls) -> "AgentRuntimePlatform":
- """Instantiate the platform of the running interpreter."""
- return cls.current_type()()
-
- @staticmethod
- def resolved_path(path: str | Path) -> Path:
- """Resolve environment and user markers without requiring existence."""
- expanded = os.path.expandvars(os.fspath(path))
- return Path(expanded).expanduser().resolve(strict=False)
-
- @staticmethod
- def process_started_at_unix(pid: int) -> float | None:
- """Return the OS-owned creation time for one local process identity."""
- import psutil
-
- try:
- return psutil.Process(pid).create_time()
- except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
- return None
-
- def _process_environment(self, pid: int) -> Mapping[str, str] | None:
- """Read a local process environment for owner-controlled projection."""
- del pid
- return None
-
- @staticmethod
- def temporary_root() -> Path:
- """Return the platform temporary directory."""
- return Path(tempfile.gettempdir()).resolve(strict=False)
-
- def current_user_id(self) -> int | None:
- """Return the POSIX user id when the platform defines one."""
- return None
-
- def supports_posix_permissions(self) -> bool:
- """Return whether descriptor ownership and mode checks are meaningful."""
- return False
-
- def child_process_environment_keys(self) -> tuple[str, ...]:
- """Return host variables required by a sanitized child process."""
- return (
- "HOME",
- "LOGNAME",
- "PATH",
- "SHELL",
- "TEMP",
- "TERM",
- "TMP",
- "TMPDIR",
- "USER",
- )
-
- def graphical_session_environment_keys(self) -> tuple[str, ...]:
- """Return platform-owned variables required by graphical children."""
- return (
- "QT_QPA_PLATFORM",
- "QT_PLUGIN_PATH",
- "QT_QPA_PLATFORM_PLUGIN_PATH",
- )
-
- def graphical_session_indicator_keys(self) -> tuple[str, ...]:
- """Return variables whose values establish an interactive GUI session."""
- return ()
-
- def graphical_session_available(
- self,
- environment: Mapping[str, str],
- ) -> bool:
- """Return whether this platform can establish a GUI from the environment."""
- indicators = self.graphical_session_indicator_keys()
- return bool(indicators) and any(environment.get(key) for key in indicators)
-
- def project_child_process_environment(
- self,
- environment: Mapping[str, str],
- *,
- include_graphical_session: bool = False,
- additional_keys: tuple[str, ...] = (),
- ) -> dict[str, str]:
- """Project one source environment through declared child-process keys."""
- graphical_keys = (
- self.graphical_session_environment_keys()
- if include_graphical_session
- else ()
- )
- allowed_keys = dict.fromkeys(
- (
- *self.child_process_environment_keys(),
- *graphical_keys,
- *additional_keys,
- )
- )
- return {key: environment[key] for key in allowed_keys if key in environment}
-
- def graphical_process_environment(
- self,
- pid: int,
- *,
- additional_keys: tuple[str, ...] = (),
- ) -> Mapping[str, str] | None:
- """Return a sanitized graphical-child projection of one process."""
- environment = self._process_environment(pid)
- if environment is None:
- return None
- return self.project_child_process_environment(
- environment,
- include_graphical_session=True,
- additional_keys=additional_keys,
- )
-
- def application_data_root(self, application_name: str) -> Path:
- """Return the user-scoped persistent data root for an application."""
- return (Path.home() / ".local" / "share" / application_name.casefold()).resolve(
- strict=False
- )
-
- def _application_runtime_candidates(
- self,
- application_name: str,
- component: Path,
- ) -> tuple[Path, ...]:
- del application_name, component
- return ()
-
- def application_runtime_dirs(
- self,
- application_name: str,
- component: str | Path,
- ) -> tuple[Path, ...]:
- """Return ordered user-scoped runtime directories for a component."""
- component_path = Path(component)
- candidates = list(
- self._application_runtime_candidates(
- application_name,
- component_path,
- )
- )
- temporary_name_parts = (
- application_name.casefold(),
- *component_path.parts,
- )
- temporary_name = "-".join(temporary_name_parts)
- user_id = self.current_user_id()
- if user_id is not None:
- temporary_name = f"{temporary_name}-{user_id}"
- candidates.append(self.temporary_root() / temporary_name)
- return tuple(dict.fromkeys(candidates))
-
-
-class OtherRuntimePlatform(AgentRuntimePlatform):
- """Portable conservative defaults for unknown host platforms."""
-
- name = "other"
-
-
-class PosixAgentRuntimePlatformMixin:
- """Reusable POSIX runtime ownership and XDG path semantics."""
-
- def current_user_id(self) -> int | None:
- return os.getuid()
-
- def supports_posix_permissions(self) -> bool:
- return self.current_user_id() is not None
-
- def graphical_session_indicator_keys(self) -> tuple[str, ...]:
- """Return POSIX display-server variables that establish a GUI session."""
- return (
- "DISPLAY",
- "WAYLAND_DISPLAY",
- )
-
- def graphical_session_environment_keys(self) -> tuple[str, ...]:
- """Return POSIX desktop-session and toolkit variables."""
- return (
- *super().graphical_session_environment_keys(),
- *self.graphical_session_indicator_keys(),
- "XAUTHORITY",
- "XDG_RUNTIME_DIR",
- "DBUS_SESSION_BUS_ADDRESS",
- "SESSION_MANAGER",
- "XDG_SESSION_TYPE",
- "XDG_SESSION_DESKTOP",
- "XDG_DATA_HOME",
- "XDG_CACHE_HOME",
- "XDG_CONFIG_HOME",
- "DESKTOP_SESSION",
- )
-
- def application_data_root(self, application_name: str) -> Path:
- configured = os.environ.get("XDG_DATA_HOME")
- data_home = (
- self.resolved_path(configured)
- if configured
- else (Path.home() / ".local" / "share").resolve(strict=False)
- )
- return data_home / application_name.casefold()
-
- def _application_runtime_candidates(
- self,
- application_name: str,
- component: Path,
- ) -> tuple[Path, ...]:
- configured = os.environ.get("XDG_RUNTIME_DIR")
- if not configured:
- return ()
- return (
- self.resolved_path(configured) / application_name.casefold() / component,
- )
-
-
-class NativeGraphicalSessionMixin:
- """Declare platforms whose desktop session needs no environment marker."""
-
- def graphical_session_available(
- self,
- environment: Mapping[str, str],
- ) -> bool:
- del environment
- return True
-
-
-class PosixRuntimePlatform(
- PosixAgentRuntimePlatformMixin,
- AgentRuntimePlatform,
-):
- """Registered authority for otherwise-unclassified POSIX hosts."""
-
- name = "posix"
-
-
-class LinuxRuntimePlatform(
- PosixAgentRuntimePlatformMixin,
- AgentRuntimePlatform,
-):
- """Linux runtime paths, including the standard per-user runtime root."""
-
- name = "linux"
- proc_root: ClassVar[Path] = Path("/proc")
-
- def _process_environment(self, pid: int) -> Mapping[str, str] | None:
- """Read a process environment for immediate owner-controlled projection."""
- try:
- payload = (self.proc_root / str(pid) / "environ").read_bytes()
- except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
- return None
-
- environment: dict[str, str] = {}
- for entry in payload.split(b"\0"):
- key, separator, value = entry.partition(b"=")
- if not separator or not key:
- continue
- environment[os.fsdecode(key)] = os.fsdecode(value)
- return environment
-
- def _application_runtime_candidates(
- self,
- application_name: str,
- component: Path,
- ) -> tuple[Path, ...]:
- candidates = list(
- super()._application_runtime_candidates(application_name, component)
- )
- user_id = self.current_user_id()
- if user_id is not None:
- candidates.append(
- Path("/run/user")
- / str(user_id)
- / application_name.casefold()
- / component
- )
- return tuple(candidates)
-
-
-class MacOSRuntimePlatform(
- NativeGraphicalSessionMixin,
- PosixAgentRuntimePlatformMixin,
- AgentRuntimePlatform,
-):
- """macOS application-data convention plus POSIX runtime ownership."""
-
- name = "macos"
-
- def application_data_root(self, application_name: str) -> Path:
- return (
- Path.home() / "Library" / "Application Support" / application_name
- ).resolve(strict=False)
-
-
-class WindowsRuntimePlatform(
- NativeGraphicalSessionMixin,
- AgentRuntimePlatform,
-):
- """Windows user-data and local runtime path conventions."""
-
- name = "windows"
-
- def child_process_environment_keys(self) -> tuple[str, ...]:
- """Preserve native Windows process, home, app-data, and temp roots."""
- return (
- *super().child_process_environment_keys(),
- "APPDATA",
- "HOMEDRIVE",
- "HOMEPATH",
- "LOCALAPPDATA",
- "SYSTEMROOT",
- "USERPROFILE",
- "WINDIR",
- )
-
- def _local_data_home(self) -> Path:
- configured = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
- if configured:
- return self.resolved_path(configured)
- return (Path.home() / "AppData" / "Local").resolve(strict=False)
-
- def application_data_root(self, application_name: str) -> Path:
- return self._local_data_home() / application_name
-
- def _application_runtime_candidates(
- self,
- application_name: str,
- component: Path,
- ) -> tuple[Path, ...]:
- return (self._local_data_home() / application_name / "runtime" / component,)
diff --git a/openhcs/agent/services/artifact_plan_inspection_service.py b/openhcs/agent/services/artifact_plan_inspection_service.py
index dc50f2ba0c..0b8ccf2db3 100644
--- a/openhcs/agent/services/artifact_plan_inspection_service.py
+++ b/openhcs/agent/services/artifact_plan_inspection_service.py
@@ -9,7 +9,8 @@
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
from python_introspect import JsonObject, to_jsonable
from openhcs.agent.dto.execution import (
ArtifactInputPlanSummary,
diff --git a/openhcs/agent/services/config_service.py b/openhcs/agent/services/config_service.py
index d8c6827132..aa464cbaab 100644
--- a/openhcs/agent/services/config_service.py
+++ b/openhcs/agent/services/config_service.py
@@ -24,7 +24,7 @@
from metaclass_registry import AutoRegisterMeta
from objectstate import get_base_type_for_lazy
-from pyqt_reactive.services.parameter_help_service import (
+from formbridge.services.parameter_help_service import (
class_docstring_text,
dataclass_parameter_descriptions,
parameter_description_from_target,
@@ -39,7 +39,8 @@
resolve_optional,
)
-from openhcs.agent.dto.common import AgentError, RenderedSource, SCHEMA_VERSION
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import RenderedSource, SCHEMA_VERSION
from openhcs.agent.dto.config import (
ConfigFieldSchema,
ConfigPatch,
diff --git a/openhcs/agent/services/endpoint_function_catalog_service.py b/openhcs/agent/services/endpoint_function_catalog_service.py
index befa6b250f..42126f129a 100644
--- a/openhcs/agent/services/endpoint_function_catalog_service.py
+++ b/openhcs/agent/services/endpoint_function_catalog_service.py
@@ -482,7 +482,7 @@ def register_custom_function(
emit_signal=False,
)
except Exception as error:
- from openhcs.agent.dto.common import AgentError
+ from formbridge.contracts import AgentError
# Native receipt is already confirmed; do not erase it on local failure.
return replace(result, errors=(AgentError.from_exception(
"custom_function_local_projection_failed", error,
diff --git a/openhcs/agent/services/function_catalog_service.py b/openhcs/agent/services/function_catalog_service.py
index d8c831a502..efb7c20ea3 100644
--- a/openhcs/agent/services/function_catalog_service.py
+++ b/openhcs/agent/services/function_catalog_service.py
@@ -13,8 +13,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar
-from pyqt_reactive.services.help_document import HelpDocument
-from pyqt_reactive.services.parameter_help_service import (
+from formbridge.services.help_document import HelpDocument
+from formbridge.services.parameter_help_service import (
dataclass_type_from_annotation,
docstring_info_for_target,
)
@@ -813,7 +813,7 @@ def admit_write(path: Path) -> Path:
def observe_custom_function_registration(
self, handle: CustomFunctionRegistrationHandle,
) -> CustomFunctionRegistrationObservation:
- from openhcs.agent.dto.common import AgentWarning
+ from formbridge.contracts import AgentWarning
from openhcs.processing.custom_functions.runtime_registry import CustomFunctionRuntimeRegistry
handle.require_current_owner()
diff --git a/openhcs/agent/services/knowledge_base_service.py b/openhcs/agent/services/knowledge_base_service.py
index a455885f0e..8de85f30f7 100644
--- a/openhcs/agent/services/knowledge_base_service.py
+++ b/openhcs/agent/services/knowledge_base_service.py
@@ -20,7 +20,8 @@
python_source_root,
)
from openhcs.agent.knowledge_manifest_schema import ComparisonManifestSnapshot
-from openhcs.agent.dto.common import AgentError, AgentWarning, SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
from python_introspect import JsonObject, JsonValue
from openhcs.agent.dto.knowledge import (
KnowledgeBaseCatalog,
diff --git a/openhcs/agent/services/llm_context_service.py b/openhcs/agent/services/llm_context_service.py
index 5e179742bb..af3d2864b7 100644
--- a/openhcs/agent/services/llm_context_service.py
+++ b/openhcs/agent/services/llm_context_service.py
@@ -57,10 +57,10 @@
)
from openhcs.agent.ui_bridge_identities import (
PipelineDebugSessionStateSurfaceIdentityDeclaration,
- PlateManagerOrchestratorCodeDocumentIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
- UiLiveOverviewStateSurfaceIdentityDeclaration,
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
)
+from formbridge.bridge.identities import UiLiveOverviewStateSurfaceIdentityDeclaration
from openhcs.ui.shared.plate_manager_code_document import (
PlateManagerDocumentAssignments,
)
@@ -197,8 +197,8 @@ class RuntimeUiCoordinationSection(
@classmethod
def render(cls, service: "AgentAuthoringContextService") -> str:
del service
- code_document_id = PlateManagerOrchestratorCodeDocumentIdentity.require_value()
- state_surface_id = PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ code_document_id = DatasetListCodeDocumentIdentity.require_value()
+ state_surface_id = DatasetListStateSurfaceIdentity.require_value()
document_fields = ", ".join(PlateManagerDocumentAssignments.names())
workflow_names = ", ".join(
operation.operation_id
diff --git a/openhcs/agent/services/object_state_field_help_service.py b/openhcs/agent/services/object_state_field_help_service.py
index b99ad03f5b..7d612f800e 100644
--- a/openhcs/agent/services/object_state_field_help_service.py
+++ b/openhcs/agent/services/object_state_field_help_service.py
@@ -7,7 +7,7 @@
from dataclasses import dataclass
from types import ModuleType
-from pyqt_reactive.services.parameter_help_service import (
+from formbridge.services.parameter_help_service import (
NO_PARAMETER_DESCRIPTION,
docstring_info_for_target,
parameter_help_content,
@@ -15,14 +15,15 @@
)
from python_introspect import JsonValue, UnifiedParameterAnalyzer, enum_input_values
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.functions import (
FunctionArtifactSpec,
FunctionDetail,
SuppliedAtRuntime,
SuppliedByAgent,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiBridgeConnectionSpec,
UiObjectStateFieldHelpQuery,
UiObjectStateFieldHelpRequest,
@@ -37,10 +38,7 @@
FunctionCatalogService,
FunctionCatalogServiceABC,
)
-from openhcs.agent.services.ui_bridge_service import (
- UiBridgeListObjectStateScopesOperation,
- UiBridgeService,
-)
+from formbridge.bridge.service import UiBridgeListObjectStateScopesOperation, UiBridgeService
@dataclass(frozen=True, slots=True)
diff --git a/openhcs/agent/services/object_state_field_projection.py b/openhcs/agent/services/object_state_field_projection.py
deleted file mode 100644
index 145ad900ce..0000000000
--- a/openhcs/agent/services/object_state_field_projection.py
+++ /dev/null
@@ -1,161 +0,0 @@
-"""Headless ObjectState field filtering and list projection policies."""
-
-from __future__ import annotations
-
-from collections.abc import Mapping
-
-from python_introspect import JsonValue, project_dataclass
-
-from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
- SemanticFields,
- UiObjectStateFieldListQuery,
- UiObjectStateFieldListResult,
- UiObjectStateFieldPathIndex,
- UiObjectStateFieldProjection,
- UiObjectStateFieldScopeProjection,
- UiObjectStateFieldSummary,
- UiObjectStateScopeCatalog,
-)
-
-
-class ObjectStateFieldListProjector:
- """Project scope catalogs through an ObjectState field query."""
-
- @classmethod
- def project_catalog(
- cls,
- query: UiObjectStateFieldListQuery,
- catalog: UiObjectStateScopeCatalog,
- ) -> UiObjectStateFieldListResult:
- matched_scopes: list[UiObjectStateFieldScopeProjection] = []
- matched_field_count = 0
- returned_field_count = 0
- source_truncated = False
- remaining_offset = query.field_offset
- requested_field_paths = set(query.field_paths)
- requested_scope_ids = set(query.scope_ids)
- contains_terms = query.contains_terms
-
- for scope in catalog.scopes:
- scope_id = scope.identity.object_state_scope_id
- if requested_scope_ids and scope_id not in requested_scope_ids:
- continue
- if scope.field_page is not None and scope.field_page.truncated:
- source_truncated = True
- field_index = UiObjectStateFieldPathIndex(tuple(scope.fields))
- container_field_paths = field_index.container_field_paths
- fields: list[UiObjectStateFieldProjection] = []
- for field in scope.fields:
- field_path = field.address.field_path
- exact_field_match = field_path in requested_field_paths
- if requested_field_paths and not exact_field_match:
- continue
- if contains_terms and not any(
- term in field_path.lower() for term in contains_terms
- ):
- continue
- if (
- not query.include_container_fields
- and not exact_field_match
- and field_path in container_field_paths
- ):
- continue
- if not query.include_clean_fields and not SemanticFields.matches(
- field
- ):
- continue
- if not query.field_filter.matches(field):
- continue
- matched_field_count += 1
- if remaining_offset:
- remaining_offset -= 1
- continue
- if query.return_limit and returned_field_count >= query.return_limit:
- continue
- fields.append(cls.project_field(query, field))
- returned_field_count += 1
- if fields:
- matched_scopes.append(
- UiObjectStateFieldScopeProjection(
- scope_id=scope_id,
- object_type=scope.object_type,
- dirty_field_count=scope.dirty_field_count,
- signature_diff_field_count=scope.signature_diff_field_count,
- has_unsaved_changes=scope.has_unsaved_changes,
- has_default_overrides=scope.has_default_overrides,
- fields=tuple(fields),
- )
- )
-
- next_offset = None
- if matched_field_count > query.field_offset + returned_field_count:
- next_offset = query.field_offset + returned_field_count
-
- return UiObjectStateFieldListResult(
- schema_version=SCHEMA_VERSION,
- object_state_token=catalog.object_state_token,
- current_branch=catalog.current_branch,
- current_snapshot_index=catalog.current_snapshot_index,
- requested_scope_ids=query.scope_ids,
- field_paths=query.field_paths,
- field_path_contains=query.field_path_contains,
- field_filter=query.field_filter,
- include_container_fields=query.include_container_fields,
- matched_scope_count=len(matched_scopes),
- matched_field_count=matched_field_count,
- returned_field_count=returned_field_count,
- field_limit=query.field_limit,
- field_offset=query.field_offset,
- next_offset=next_offset,
- truncated=(
- source_truncated
- or matched_field_count > query.field_offset + returned_field_count
- ),
- scopes=tuple(matched_scopes),
- errors=catalog.errors,
- warnings=catalog.warnings,
- )
-
- @classmethod
- def project_field(
- cls,
- query: UiObjectStateFieldListQuery,
- field: UiObjectStateFieldSummary,
- ) -> UiObjectStateFieldProjection:
- return project_dataclass(
- UiObjectStateFieldProjection,
- field,
- field_path=field.address.field_path,
- raw_value=cls.compact_value(query, field.raw_value),
- resolved_value=cls.compact_value(query, field.resolved_value),
- )
-
- @staticmethod
- def compact_value(
- query: UiObjectStateFieldListQuery,
- value: JsonValue | None,
- ) -> JsonValue | None:
- if isinstance(value, str) and len(value) > query.max_value_chars:
- return {
- "type": "str",
- "length": len(value),
- "truncated": True,
- "preview": value[: query.max_value_chars],
- }
- if isinstance(value, (list, tuple)) and len(value) > query.max_value_items:
- return {
- "type": "sequence",
- "length": len(value),
- "truncated": True,
- "preview": tuple(value[: query.max_value_items]),
- }
- if isinstance(value, Mapping) and len(value) > query.max_value_items:
- preview = tuple(value.items())[: query.max_value_items]
- return {
- "type": "mapping",
- "length": len(value),
- "truncated": True,
- "preview": {str(key): item for key, item in preview},
- }
- return value
diff --git a/openhcs/agent/services/pipeline_authoring_service.py b/openhcs/agent/services/pipeline_authoring_service.py
index 4a6359cfc1..859d969f1c 100644
--- a/openhcs/agent/services/pipeline_authoring_service.py
+++ b/openhcs/agent/services/pipeline_authoring_service.py
@@ -10,12 +10,8 @@
from objectstate import get_base_type_for_lazy
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
- AgentError,
- AgentWarning,
- RenderedSource,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION, RenderedSource
+from formbridge.contracts import AgentError, AgentWarning
from python_introspect import JsonObject, JsonValue
from openhcs.agent.dto.config import ConfigPatch
from openhcs.agent.dto.pipeline import (
diff --git a/openhcs/agent/services/plate_inspection_service.py b/openhcs/agent/services/plate_inspection_service.py
index df564102cf..03510bcee7 100644
--- a/openhcs/agent/services/plate_inspection_service.py
+++ b/openhcs/agent/services/plate_inspection_service.py
@@ -10,7 +10,8 @@
from polystore.base import ImageSamplingRequest
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
-from openhcs.agent.dto.common import AgentError, AgentWarning, SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
from python_introspect import JsonObject
from openhcs.agent.dto.plate import (
PlateFileQueryRecordSummary,
@@ -45,9 +46,7 @@
)
from openhcs.agent.path_policy import AgentPathPolicy, AgentPathPolicyError
from openhcs.authoring.session.operations.datasets import InitializeDatasets
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerOrchestratorCodeDocumentIdentity,
-)
+from openhcs.agent.ui_bridge_identities import DatasetListCodeDocumentIdentity
from openhcs.constants.constants import Backend
from openhcs.core.config import GlobalPipelineConfig, PathPlanningConfig
from openhcs.core.plate_image_inventory import (
@@ -939,7 +938,7 @@ def _advice(
ingestion_owner=ingestion_owner,
source_binding_role=source_binding_role,
ui_code_document_id=(
- PlateManagerOrchestratorCodeDocumentIdentity.require_value()
+ DatasetListCodeDocumentIdentity.require_value()
),
ui_operation=InitializeDatasets.operation_id,
knowledge_query=cls.KNOWLEDGE_QUERY,
diff --git a/openhcs/agent/services/plate_streaming_service.py b/openhcs/agent/services/plate_streaming_service.py
index db62faf29c..353cf5176f 100644
--- a/openhcs/agent/services/plate_streaming_service.py
+++ b/openhcs/agent/services/plate_streaming_service.py
@@ -15,24 +15,22 @@
)
from polystore.streaming.viewer_transport import ViewerStreamProducer
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
from python_introspect import JsonObject
from openhcs.agent.dto.plate import (
PlateFileStreamRequest,
PlateFileStreamResult,
PlatePathInspectionRequest,
)
-from openhcs.agent.dto.ui_bridge import UiBridgeConnectionSpec
+from formbridge.bridge.dto import UiBridgeConnectionSpec
from openhcs.agent.dto.viewer import ViewerWindowStateResult
from openhcs.agent.services.plate_inspection_service import (
PlateInspectionContext,
PlateInspectionFileQueryProjection,
PlateInspectionService,
)
-from openhcs.agent.services.ui_bridge_service import (
- DEFAULT_UI_BRIDGE_CONNECTION_SPEC,
- UiBridgeService,
-)
+from formbridge.bridge.service import DEFAULT_UI_BRIDGE_CONNECTION_SPEC, UiBridgeService
from openhcs.constants import Backend
from openhcs.core.config import StreamingConfig
from openhcs.core.plate_image_inventory import (
diff --git a/openhcs/agent/services/runtime_server_service.py b/openhcs/agent/services/runtime_server_service.py
index dc3060b7c8..889d7a9a0c 100644
--- a/openhcs/agent/services/runtime_server_service.py
+++ b/openhcs/agent/services/runtime_server_service.py
@@ -5,7 +5,8 @@
from abc import ABC, abstractmethod
from dataclasses import replace
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
from python_introspect import JsonObject, registered_class_from_key, to_jsonable
from openhcs.agent.dto.execution import (
RuntimeDebugArtifactExportRequest,
diff --git a/openhcs/agent/services/selected_plate_service.py b/openhcs/agent/services/selected_plate_service.py
index 46a6d98e64..89f927d0d5 100644
--- a/openhcs/agent/services/selected_plate_service.py
+++ b/openhcs/agent/services/selected_plate_service.py
@@ -5,8 +5,9 @@
from collections.abc import Mapping
from dataclasses import dataclass
-from openhcs.core.selection import SelectedItems
-from openhcs.agent.dto.common import AgentError, AgentWarning, SCHEMA_VERSION
+from formbridge.selection import SelectedItems
+from formbridge.contracts import AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
from python_introspect import JsonValue
from openhcs.agent.dto.plate import (
OutputPlateRoot,
@@ -22,20 +23,15 @@
SelectedPlateImageSampleRequest,
SelectedPlateImageSampleResult,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiBridgeConnectionSpec,
UiStateSurfaceDocument,
UiStateSurfaceRequest,
)
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerStateSurfaceIdentityDeclaration,
-)
+from openhcs.agent.ui_bridge_identities import DatasetListStateSurfaceIdentity
from openhcs.agent.services.plate_inspection_service import PlateInspectionService
from openhcs.agent.services.plate_streaming_service import PlateStreamingService
-from openhcs.agent.services.ui_bridge_service import (
- UiBridgeGetStateSurfaceOperation,
- UiBridgeService,
-)
+from formbridge.bridge.service import UiBridgeGetStateSurfaceOperation, UiBridgeService
from openhcs.core.plate_image_inventory import PlateFileKind
@@ -290,7 +286,7 @@ def selected_plate_state(
state_document = self.ui_bridge_service.invoke(
UiBridgeGetStateSurfaceOperation,
UiStateSurfaceRequest(
- surface_id=PlateManagerStateSurfaceIdentityDeclaration.require_value(),
+ surface_id=DatasetListStateSurfaceIdentity.require_value(),
selection_mode=SelectedItems.name,
),
connection,
@@ -325,7 +321,7 @@ def selected_plate_target_root(
target: type[SelectedPlateFileQueryTarget],
source_format: str,
) -> tuple[SelectedPlateTargetRoot | None, AgentError | None]:
- state_surface_id = PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ state_surface_id = DatasetListStateSurfaceIdentity.require_value()
plate_root = selected_plate.plate_root
if plate_root is None:
raise RuntimeError("Selected row plate_root was validated as a string.")
@@ -347,7 +343,7 @@ def selected_plate_target_root(
def selected_plate_row_from_state_surface(
state_document: UiStateSurfaceDocument,
) -> tuple[Mapping[str, JsonValue] | None, AgentError | None]:
- state_surface_id = PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ state_surface_id = DatasetListStateSurfaceIdentity.require_value()
selected_scope_ids = state_document.selected_scope_ids
if len(selected_scope_ids) != 1:
return None, AgentError(
diff --git a/openhcs/agent/services/synthetic_plate_service.py b/openhcs/agent/services/synthetic_plate_service.py
index b8b9ac1faa..076304cb83 100644
--- a/openhcs/agent/services/synthetic_plate_service.py
+++ b/openhcs/agent/services/synthetic_plate_service.py
@@ -5,7 +5,8 @@
from enum import Enum
from pathlib import Path
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.plate import (
PlatePathInspectionRequest,
PlatePathInspectionResult,
diff --git a/openhcs/agent/services/ui_bridge_service.py b/openhcs/agent/services/ui_bridge_service.py
index 0bc8e9104f..f4c5004857 100644
--- a/openhcs/agent/services/ui_bridge_service.py
+++ b/openhcs/agent/services/ui_bridge_service.py
@@ -1,661 +1,44 @@
-"""Agent service boundary for a running OpenHCS PyQt UI bridge."""
+"""The OpenHCS UI-bridge operations and its bridge connection environment."""
from __future__ import annotations
-import json
-import os
-import stat
-import time
-from abc import ABC, abstractmethod
-from collections.abc import Callable
-from dataclasses import dataclass, replace
+from dataclasses import dataclass
from os import environ
-from pathlib import Path
-from typing import Annotated, ClassVar
-
-from metaclass_registry import AutoRegisterMeta
-from python_introspect import (
- EnvironmentVariable,
- JsonObject,
- dataclass_from_mapping,
- overlay_dataclass_from_environment,
- project_dataclass,
-)
+from typing import Annotated
+from python_introspect import EnvironmentVariable
from zmqruntime.config import (
NonBlankString,
PositiveInteger,
SocketPort,
TransportMode,
)
-
-from openhcs.core.selection import AllItems, SelectedItems
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.session import SessionEventBatch, SessionEventsRequest
-from openhcs.agent.dto.ui_bridge import (
- ActionAccepted,
+from formbridge.bridge.dto import (
ActionUnavailable,
- OperationRunning,
- OperationUnavailable,
UI_BRIDGE_UNKNOWN_WIDGET,
- UNKNOWN_UI_BRIDGE_OPERATION_ROUTE,
- UiActionCatalog,
UiActionIdentity,
- UiActionInvokeRequest,
UiActionInvokeResult,
- UiBranchCatalog,
- UiBranchSwitchRequest,
- UiBridgeCatalog,
UiBridgeConnectionFields,
UiBridgeConnectionSpec,
- UiBridgeDescriptorFile,
- UiBridgeDescriptorSummary,
- UiBridgeDescriptorWirePayload,
- UiBridgeEndpointIdentity,
- UiBridgeOperationIdentity,
- UiBridgeOperationRef,
- UiBridgeOperationStatus,
- UiBridgeOperationStatusRequest,
- UiBridgeOperationWaitRequest,
- UiBridgeStatus,
- UiCodeDocument,
- UiCodeDocumentApplyRequest,
- UiCodeDocumentApplyResult,
- UiCodeDocumentCatalog,
- UiCodeDocumentIdentity,
- UiCodeDocumentRequest,
- UiCodeDocumentSummary,
- UiCodeDocumentValidationRequest,
- UiCodeDocumentValidationResult,
UiMutationReceipt,
- UiMutationRequestToken,
- UiObjectStateFieldListQuery,
- UiObjectStateFieldListResult,
- UiObjectStateFieldMutationRequest,
- UiObjectStateFieldMutationResult,
- UiObjectStateScopeCatalog,
- UiObjectStateScopeListRequest,
+)
+from openhcs.agent.dto.ui_bridge import (
UiSelectedPlateWorkflowRequest,
UiSelectedPlateWorkflowResult,
- UiSnapshotCatalog,
- UiSnapshotListRequest,
- UiSnapshotRestoreRequest,
- UiSnapshotRestoreResult,
- UiStateSurfaceCatalog,
- UiStateSurfaceDocument,
- UiStateSurfaceIdentity,
- UiStateSurfaceRequest,
- UiStateSurfaceSummary,
- UiTimeTravelHeadRequest,
- UiWidgetActionInvokeRequest,
- UiWidgetActionInvokeResult,
- UiWidgetTreeRequest,
- UiWidgetTreeResult,
- UiWindowCatalog,
- UiWindowCloseRequest,
- UiWindowCloseResult,
- UiWindowFocusRequest,
- UiWindowFocusResult,
- UiWindowNavigateRequest,
- UiWindowNavigateResult,
- UiWindowSnapshotRequest,
- UiWindowSnapshotResult,
)
-from openhcs.agent.path_policy import AgentPathPolicy, AgentPathPolicyError
-from openhcs.agent.runtime_platform import AgentRuntimePlatform
-from openhcs.agent.services.object_state_field_projection import (
- ObjectStateFieldListProjector,
+import openhcs.agent.ui_bridge_application # noqa: F401 (declares the OpenHCS bridge application)
+from formbridge.bridge.service import (
+ DEFAULT_UI_BRIDGE_CONNECTION_SPEC,
+ UiBridgeOperation,
+ UiBridgeService,
)
-from openhcs.agent.ui_bridge_environment import UiBridgeDescriptorEnvironment
+from formbridge.runtime_platform import AgentRuntimePlatform
+
from openhcs.core.native_threading import native_thread_count_environment_keys
from openhcs.runtime.viewer_protocol import ViewerLaunchContext
-from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
from openhcs.utils.environment import OpenHCSProcessEnvironment
-UI_BRIDGE_PROTOCOL_VERSION = "openhcs.ui_bridge.v3"
-DEFAULT_UI_BRIDGE_CONNECTION_SPEC = UiBridgeConnectionSpec()
-DEFAULT_UI_BRIDGE_TIMEOUT_MS = DEFAULT_UI_BRIDGE_CONNECTION_SPEC.timeout_ms
-UNAVAILABLE_UI_CODE_DOCUMENT_TITLE = "Unavailable UI code document"
-UNAVAILABLE_UI_STATE_SURFACE_TITLE = "Unavailable UI state surface"
-
-
-@dataclass(slots=True)
-class UiBridgeRequestRejected(Exception):
- """A request the client refuses before it reaches the bridge."""
-
- errors: tuple[AgentError, ...]
-
-
-class UiBridgeOperation(ABC, metaclass=AutoRegisterMeta):
- """One running-UI operation: its request, result and failure answer.
-
- The class is the operation. Gateways, ``UiBridgeService`` and the MCP
- capabilities are generic over it; the running UI serves it with one
- ``@serves`` method. Operations with a ``name`` cross the wire and are
- registered under it; operations without one are composed on the client.
- """
-
- __registry_key__ = "name"
- __skip_if_no_key__ = True
-
- name: ClassVar[str | None] = None
- request_type: ClassVar[type]
- result_type: ClassVar[type]
- requires_auth: ClassVar[bool] = True
- bridge_feature: ClassVar[str | None] = None
- success_outcome: ClassVar[str] = "completed"
- failure_error_code: ClassVar[str] = "ui_bridge_operation_failed"
-
- @classmethod
- def for_name(cls, operation_name: str) -> type["UiBridgeOperation"]:
- try:
- return cls.__registry__[operation_name]
- except KeyError as exc:
- raise KeyError(f"Unknown UI bridge operation: {operation_name}") from exc
-
- @classmethod
- def require_name(cls) -> str:
- if cls.name is None:
- raise ValueError(f"UI bridge operation {cls.__qualname__} has no name.")
- return cls.name
-
- @classmethod
- def supported_operation_names(cls) -> tuple[str, ...]:
- return tuple(cls.__registry__)
-
- @classmethod
- def supported_bridge_features(cls) -> tuple[str, ...]:
- return tuple(
- dict.fromkeys(
- operation.bridge_feature
- for operation in cls.__registry__.values()
- if operation.bridge_feature is not None
- )
- )
-
- # -- request on the wire ------------------------------------------------
-
- @classmethod
- def decode_request(cls, payload: JsonObject):
- return dataclass_from_mapping(cls.request_type, payload)
-
- @classmethod
- def validate_request(cls, request) -> None:
- if not isinstance(request, cls.request_type):
- raise TypeError(
- f"UI bridge operation {cls.name!r} requires "
- f"{cls.request_type.__name__} request."
- )
-
- @classmethod
- def serve(cls, handler: Callable, request):
- """Run the running UI's implementation of this operation."""
- return handler(request)
-
- @classmethod
- def as_served(cls, result, binding: UiBridgeConnectionSpec):
- """The result as the bridge server answers it."""
- del binding
- return result
-
- @classmethod
- def accepted(cls, request, bridge_operation: UiBridgeOperationRef):
- """The placeholder a queued mutation answers before it completes."""
- raise NotImplementedError(f"{cls.__name__} is not a queued mutation.")
-
- @classmethod
- def outcome(cls, result) -> str:
- """The tracker outcome of one completed mutation."""
- del result
- return cls.success_outcome
-
- # -- client side -------------------------------------------------------
-
- @classmethod
- def failed(cls, request, errors: tuple[AgentError, ...]):
- """The result reporting that the operation could not run."""
- raise NotImplementedError(f"{cls.__name__} declares no failure answer.")
-
- @classmethod
- def prepare(cls, service: "UiBridgeService", request):
- """Check or rewrite the request before it leaves the client."""
- del service
- return request
-
- @classmethod
- def call(cls, service: "UiBridgeService", connection, request):
- return service.gateway.invoke(connection, cls, request)
-
- @classmethod
- def respond(cls, service: "UiBridgeService", connection, request):
- try:
- request = cls.prepare(service, request)
- except UiBridgeRequestRejected as rejected:
- return cls.failed(request, rejected.errors)
- resolution = service.resolve(connection)
- if not resolution.ok:
- return cls.failed(request, resolution.errors)
- try:
- return cls.call(service, resolution, request)
- except Exception as exc:
- return cls.failed(
- request, ui_bridge_gateway_errors(exc, "ui_bridge_unavailable")
- )
-
-
-class UiBridgeRequestlessOperation(UiBridgeOperation):
- """Operation that takes no request."""
-
- request_type: ClassVar[None] = None
-
- @classmethod
- def decode_request(cls, payload: JsonObject) -> None:
- if payload:
- raise ValueError(
- f"UI bridge operation {cls.name!r} does not accept a payload."
- )
- return None
-
- @classmethod
- def validate_request(cls, request) -> None:
- if request is not None:
- raise TypeError(
- f"UI bridge operation {cls.name!r} does not accept a request."
- )
-
- @classmethod
- def serve(cls, handler: Callable, request):
- del request
- return handler()
-
-
-def serves(operation: type[UiBridgeOperation]) -> Callable[[Callable], Callable]:
- """Mark the running UI's implementation of one wire operation."""
-
- def mark(handler: Callable) -> Callable:
- handler.served_operation = operation
- return handler
-
- return mark
-
-
-class UiBridgeOperationServer:
- """Implementation side of the family: one ``@serves`` method per operation."""
-
- served_operations: ClassVar[dict[type[UiBridgeOperation], Callable]] = {}
-
- def __init_subclass__(cls, **kwargs) -> None:
- super().__init_subclass__(**kwargs)
- cls.served_operations = {
- handler.served_operation: handler
- for klass in reversed(cls.__mro__)
- for handler in vars(klass).values()
- if hasattr(handler, "served_operation")
- }
-
- def invoke(self, operation: type[UiBridgeOperation], request=None):
- handler = self.served_operations[operation]
- return operation.serve(handler.__get__(self), request)
-
-
-# -- operation groups: the status feature tag and shared failure answers -----
-
-
-class UiCodeDocumentOperation(UiBridgeOperation):
- bridge_feature = "ui_code_documents"
-
-
-class UiStateSurfaceOperation(UiBridgeOperation):
- bridge_feature = "ui_state_surfaces"
-
-
-class UiActionOperation(UiBridgeOperation):
- bridge_feature = "ui_actions"
-
-
-class UiWindowOperation(UiBridgeOperation):
- bridge_feature = "ui_windows"
-
-
-class UiObjectStateScopeOperation(UiBridgeOperation):
- bridge_feature = "objectstate_scopes"
-
-
-class UiObjectStateSnapshotOperation(UiBridgeOperation):
- bridge_feature = "objectstate_snapshots"
-
-
-class UiObjectStateRestoreOperation(UiBridgeOperation):
- """Operations that move the running UI to another ObjectState snapshot."""
-
- result_type = UiSnapshotRestoreResult
-
- @classmethod
- def failed(cls, request, errors):
- del request
- return UiSnapshotRestoreResult(
- schema_version=SCHEMA_VERSION,
- restored=False,
- target_snapshot=None,
- current_snapshot=None,
- errors=errors,
- )
-
- @classmethod
- def accepted(cls, request, bridge_operation):
- del request
- return UiSnapshotRestoreResult(
- schema_version=SCHEMA_VERSION,
- restored=False,
- target_snapshot=None,
- operation_id=bridge_operation.identity.operation_id,
- receipt=UiMutationReceipt.accepted_for(
- UiMutationRequestToken(),
- bridge_operation_id=bridge_operation.identity.operation_id,
- ),
- )
-
- @classmethod
- def outcome(cls, result) -> str:
- return "restored" if result.restored else "not_restored"
-
-
-class UiBridgeOperationStatusQuery(UiBridgeOperation):
- """Operations answering with one bridge operation's status."""
-
- result_type = UiBridgeOperationRef
-
- @classmethod
- def failed(cls, request, errors):
- return UiBridgeOperationRef(
- schema_version=SCHEMA_VERSION,
- identity=UiBridgeOperationIdentity(
- operation_id=request.operation_id,
- route=UNKNOWN_UI_BRIDGE_OPERATION_ROUTE,
- ),
- status=OperationUnavailable.name,
- started_at_unix=0.0,
- errors=errors,
- )
-
-
-# -- the operations ----------------------------------------------------------
-
-
-class UiBridgeStatusOperation(UiBridgeRequestlessOperation):
- name = "status"
- result_type = UiBridgeStatus
- requires_auth = False
-
- @classmethod
- def as_served(cls, result: UiBridgeStatus, binding: UiBridgeConnectionSpec):
- return replace(
- result,
- auth_required=True,
- bridge_instance_id=binding.bridge_instance_id,
- connection=binding.public_connection(),
- descriptor_file_path=binding.descriptor_file_path,
- supported_operations=UiBridgeOperation.supported_operation_names(),
- provider_catalog_schema_versions=(SCHEMA_VERSION,),
- bridge_features=UiBridgeOperation.supported_bridge_features(),
- )
-
- @classmethod
- def failed(cls, request, errors):
- del request
- return UiBridgeStatus(schema_version=SCHEMA_VERSION, reachable=False, errors=errors)
-
- @staticmethod
- def _unreachable(resolution: "UiBridgeConnectionResolution") -> UiBridgeStatus:
- return UiBridgeStatus(
- schema_version=SCHEMA_VERSION,
- reachable=False,
- connection=resolution.public_connection(),
- descriptor_file_path=resolution.descriptor_file_path,
- descriptor_status=resolution.descriptor.status,
- descriptors=resolution.descriptor.summaries,
- errors=resolution.errors,
- )
-
- @classmethod
- def respond(cls, service, connection, request):
- resolution = service.resolve(connection)
- if not resolution.ok:
- return cls._unreachable(resolution)
- try:
- status = cls.call(service, resolution, request)
- except Exception as exc:
- return cls._unreachable(
- UiBridgeConnectionResolution.from_connection(
- resolution,
- descriptor=resolution.descriptor,
- errors=ui_bridge_gateway_errors(exc, "ui_bridge_unreachable"),
- )
- )
- validation_errors = resolution.validate_live_status(status)
- if validation_errors:
- return cls._unreachable(
- UiBridgeConnectionResolution.from_connection(
- resolution,
- descriptor=replace(
- resolution.descriptor,
- status=validation_errors[0].code,
- ),
- errors=validation_errors,
- )
- )
- return resolution.descriptor.project_status(status, connection=resolution)
-
-
-class UiBridgeListDocumentsOperation(
- UiBridgeRequestlessOperation, UiCodeDocumentOperation
-):
- name = "list_documents"
- result_type = UiCodeDocumentCatalog
-
- @classmethod
- def failed(cls, request, errors):
- return UiCodeDocumentCatalog(SCHEMA_VERSION, documents=(), errors=errors)
-
-
-class UiBridgeListStateSurfacesOperation(
- UiBridgeRequestlessOperation, UiStateSurfaceOperation
-):
- name = "list_state_surfaces"
- result_type = UiStateSurfaceCatalog
-
- @classmethod
- def failed(cls, request, errors):
- return UiStateSurfaceCatalog(SCHEMA_VERSION, surfaces=(), errors=errors)
-
-
-class UiBridgeListActionsOperation(UiBridgeRequestlessOperation, UiActionOperation):
- name = "list_actions"
- result_type = UiActionCatalog
-
- @classmethod
- def failed(cls, request, errors):
- return UiActionCatalog(SCHEMA_VERSION, actions=(), errors=errors)
-
-
-class UiBridgeListWindowsOperation(UiBridgeRequestlessOperation, UiWindowOperation):
- name = "list_windows"
- result_type = UiWindowCatalog
-
- @classmethod
- def failed(cls, request, errors):
- return UiWindowCatalog(schema_version=SCHEMA_VERSION, windows=(), errors=errors)
-
-
-class UiBridgeListObjectStateScopesOperation(UiObjectStateScopeOperation):
- name = "list_object_state_scopes"
- request_type = UiObjectStateScopeListRequest
- result_type = UiObjectStateScopeCatalog
-
- @classmethod
- def failed(cls, request, errors):
- del request
- return UiObjectStateScopeCatalog(
- schema_version=SCHEMA_VERSION,
- object_state_token=0,
- current_branch="",
- current_snapshot_index=-1,
- active=False,
- scopes=(),
- errors=errors,
- )
-
- @classmethod
- def respond(cls, service, connection, request):
- return request.filtered_catalog(super().respond(service, connection, request))
-
-
-class UiBridgeGetObjectStateFieldsOperation(UiObjectStateScopeOperation):
- """Field rows projected on the client from the scope catalog."""
-
- request_type = UiObjectStateFieldListQuery
- result_type = UiObjectStateFieldListResult
-
- @classmethod
- def respond(cls, service, connection, request):
- catalog = UiBridgeListObjectStateScopesOperation.respond(
- service, connection, request.scope_list_request()
- )
- return ObjectStateFieldListProjector.project_catalog(request, catalog)
-
-
-class UiBridgeMutateObjectStateFieldOperation(UiBridgeOperation):
- name = "mutate_object_state_field"
- request_type = UiObjectStateFieldMutationRequest
- result_type = UiObjectStateFieldMutationResult
- bridge_feature = "objectstate_field_mutation"
-
- @classmethod
- def failed(cls, request, errors):
- return UiObjectStateFieldMutationResult(
- schema_version=SCHEMA_VERSION,
- address=request,
- mutated=False,
- reset=request.reset,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=errors,
- )
-
- @classmethod
- def accepted(cls, request, bridge_operation):
- return UiObjectStateFieldMutationResult(
- schema_version=SCHEMA_VERSION,
- address=request,
- mutated=False,
- reset=request.reset,
- receipt=UiMutationReceipt.accepted_for(
- request.request_token,
- bridge_operation_id=bridge_operation.identity.operation_id,
- ),
- )
-
- @classmethod
- def outcome(cls, result) -> str:
- return "mutated" if result.mutated else "not_mutated"
-
-
-class UiBridgeGetDocumentOperation(UiCodeDocumentOperation):
- name = "get_document"
- request_type = UiCodeDocumentRequest
- result_type = UiCodeDocument
-
- @classmethod
- def failed(cls, request, errors):
- return UiCodeDocument(
- schema_version=SCHEMA_VERSION,
- summary=UiCodeDocumentSummary(
- schema_version=SCHEMA_VERSION,
- identity=UiCodeDocumentIdentity(document_id=request.document_id),
- title=UNAVAILABLE_UI_CODE_DOCUMENT_TITLE,
- widget_id=UI_BRIDGE_UNKNOWN_WIDGET,
- readable=False,
- writable=False,
- ),
- source="",
- mime_type="text/x-python",
- size_bytes=0,
- sha256="",
- current_revision_token=None,
- current_snapshot=None,
- selection_mode=request.resolved_selection_mode(
- SelectedItems
- ),
- selected_scope_ids=(),
- errors=errors,
- )
-
-
-class UiBridgeGetStateSurfaceOperation(UiStateSurfaceOperation):
- name = "get_state_surface"
- request_type = UiStateSurfaceRequest
- result_type = UiStateSurfaceDocument
-
- @classmethod
- def failed(cls, request, errors):
- return UiStateSurfaceDocument(
- schema_version=SCHEMA_VERSION,
- summary=UiStateSurfaceSummary(
- schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id=request.surface_id),
- title=UNAVAILABLE_UI_STATE_SURFACE_TITLE,
- widget_id=UI_BRIDGE_UNKNOWN_WIDGET,
- readable=False,
- ),
- payload_schema="openhcs.ui.unavailable_state_surface.v1",
- payload={},
- selection_mode=request.resolved_selection_mode(
- AllItems
- ),
- selected_scope_ids=(),
- current_revision_token=None,
- current_snapshot=None,
- errors=errors,
- )
-
-
-class UiBridgeInvokeActionOperation(UiActionOperation):
- name = "invoke_action"
- request_type = UiActionInvokeRequest
- result_type = UiActionInvokeResult
-
- @classmethod
- def failed(cls, request, errors):
- return UiActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- identity=UiActionIdentity(
- widget_id=request.widget_id,
- action_id=request.action_id,
- ),
- status=ActionUnavailable,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=errors,
- )
-
- @classmethod
- def accepted(cls, request, bridge_operation):
- return UiActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- identity=UiActionIdentity(
- widget_id=request.widget_id,
- action_id=request.action_id,
- ),
- status=ActionAccepted,
- receipt=UiMutationReceipt.accepted_for(
- request.request_token,
- bridge_operation_id=bridge_operation.identity.operation_id,
- ),
- target_scope_ids=request.selected_scope_ids,
- selection_revision_token=request.observed_selection_revision_token,
- )
-
- @classmethod
- def outcome(cls, result) -> str:
- return result.status.name
-
class UiBridgeSelectedPlateWorkflowOperation(UiBridgeOperation):
name = "selected_plate_workflow"
@@ -697,1123 +80,42 @@ def failed(cls, request, errors):
)
-class UiBridgeFocusWindowOperation(UiWindowOperation):
- name = "focus_window"
- request_type = UiWindowFocusRequest
- result_type = UiWindowFocusResult
-
- @classmethod
- def failed(cls, request, errors):
- return UiWindowFocusResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=False,
- errors=errors,
- )
-
-
-class UiBridgeNavigateWindowOperation(UiBridgeOperation):
- name = "navigate_window"
- request_type = UiWindowNavigateRequest
- result_type = UiWindowNavigateResult
- bridge_feature = "ui_window_navigation"
- success_outcome = "navigated"
- failure_error_code = "ui_window_navigation_failed"
-
- @classmethod
- def failed(cls, request, errors):
- return UiWindowNavigateResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=False,
- navigated=False,
- created=False,
- errors=errors,
- )
-
+@dataclass(frozen=True, slots=True)
+class UiBridgeEnvironment(UiBridgeConnectionFields):
+ """The environment variables that override a UI bridge client's connection."""
-class UiBridgeCloseWindowOperation(UiWindowOperation):
- name = "close_window"
- request_type = UiWindowCloseRequest
- result_type = UiWindowCloseResult
+ host: Annotated[
+ NonBlankString | None,
+ EnvironmentVariable("OPENHCS_UI_BRIDGE_HOST"),
+ ] = None
+ port: Annotated[
+ SocketPort | None,
+ EnvironmentVariable("OPENHCS_UI_BRIDGE_PORT"),
+ ] = None
+ transport_mode: Annotated[
+ TransportMode | None,
+ EnvironmentVariable("OPENHCS_UI_BRIDGE_TRANSPORT_MODE"),
+ ] = None
+ timeout_ms: Annotated[
+ PositiveInteger | None,
+ EnvironmentVariable("OPENHCS_UI_BRIDGE_TIMEOUT_MS"),
+ ] = None
+ auth_token: Annotated[
+ NonBlankString | None,
+ EnvironmentVariable("OPENHCS_UI_BRIDGE_AUTH_TOKEN"),
+ ] = None
- @classmethod
- def failed(cls, request, errors):
- return UiWindowCloseResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- closed=False,
- errors=errors,
- )
-
-
-class UiBridgeSnapshotWindowOperation(UiBridgeOperation):
- name = "snapshot_window"
- request_type = UiWindowSnapshotRequest
- result_type = UiWindowSnapshotResult
- bridge_feature = "ui_window_snapshots"
- success_outcome = "captured"
- failure_error_code = "ui_window_snapshot_failed"
-
- @classmethod
- def failed(cls, request, errors):
- return project_dataclass(
- UiWindowSnapshotResult,
- request,
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- captured=False,
- errors=errors,
- )
-
- @classmethod
- def prepare(cls, service, request):
- try:
- output_dir = service.path_policy.assert_writable(request.output_dir_path)
- except AgentPathPolicyError as exc:
- raise UiBridgeRequestRejected((exc.to_agent_error(),)) from exc
- return replace(request, output_dir_path=str(output_dir))
-
-
-class UiBridgeWidgetTreeOperation(UiBridgeOperation):
- name = "widget_tree"
- request_type = UiWidgetTreeRequest
- result_type = UiWidgetTreeResult
- bridge_feature = "widget_tree_projection"
-
- @classmethod
- def failed(cls, request, errors):
- return UiWidgetTreeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- projected=False,
- errors=errors,
- )
-
-
-class UiBridgeGetOperationStatusOperation(UiBridgeOperationStatusQuery):
- name = "get_operation_status"
- request_type = UiBridgeOperationStatusRequest
- bridge_feature = "operation_status"
-
-
-class UiBridgeWaitForOperationReceiptOperation(UiBridgeOperationStatusQuery):
- """Wait on the client for an operation to reach a terminal status."""
-
- request_type = UiBridgeOperationWaitRequest
-
- @classmethod
- def call(cls, service, connection, request):
- deadline = time.monotonic() + request.timeout_seconds
- status_request = UiBridgeOperationStatusRequest(
- operation_id=request.operation_id
- )
- while True:
- operation = UiBridgeGetOperationStatusOperation.call(
- service, connection, status_request
- )
- try:
- status = UiBridgeOperationStatus.named(operation.status)
- except ValueError:
- return replace(
- operation,
- errors=(
- *operation.errors,
- AgentError(
- code="invalid_ui_bridge_operation_status",
- message=(
- f"UI bridge operation {request.operation_id!r} returned "
- f"unknown status {operation.status!r}."
- ),
- ),
- ),
- )
- if status.is_terminal:
- return operation
- remaining_seconds = deadline - time.monotonic()
- if remaining_seconds <= 0.0:
- return replace(
- operation,
- errors=(
- *operation.errors,
- AgentError(
- code="ui_bridge_operation_wait_timeout",
- message=(
- f"UI bridge operation {request.operation_id!r} did not "
- f"reach a terminal status within "
- f"{request.timeout_seconds:g} seconds."
- ),
- hint=(
- "The bridge mutation receipt remains active; inspect "
- "it with openhcs_ui_get_operation_status or call "
- "openhcs_ui_wait_for_operation_receipt again. Receipt "
- "completion does not establish domain workflow "
- "completion."
- ),
- ),
- ),
- )
- time.sleep(min(request.poll_interval_seconds, remaining_seconds))
-
-
-class UiBridgeInvokeWidgetActionOperation(UiBridgeOperation):
- name = "invoke_widget_action"
- request_type = UiWidgetActionInvokeRequest
- result_type = UiWidgetActionInvokeResult
- bridge_feature = "widget_action_invocation"
-
- @classmethod
- def failed(cls, request, errors):
- return UiWidgetActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- path_id=request.path_id,
- action_kind=request.action_kind,
- invoked=False,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=errors,
- )
-
- @classmethod
- def accepted(cls, request, bridge_operation):
- return UiWidgetActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- path_id=request.path_id,
- action_kind=request.action_kind,
- invoked=False,
- receipt=UiMutationReceipt.accepted_for(
- request.request_token,
- bridge_operation_id=bridge_operation.identity.operation_id,
- ),
- )
-
- @classmethod
- def outcome(cls, result) -> str:
- return result.outcome.value
-
- @classmethod
- def call(cls, service, connection, request):
- """Return the terminal fact an accepted action receipt stands for."""
- result = super().call(service, connection, request)
- operation_id = result.receipt.bridge_operation_id
- if result.invoked or not result.receipt.accepted or operation_id is None:
- return result
- operation = UiBridgeWaitForOperationReceiptOperation.call(
- service,
- connection,
- UiBridgeOperationWaitRequest(
- operation_id=operation_id,
- timeout_seconds=min(connection.timeout_ms / 1000.0, 120.0),
- poll_interval_seconds=0.05,
- ),
- )
- return result.resolve_operation(operation)
-
-
-class UiBridgeValidateDocumentOperation(UiCodeDocumentOperation):
- name = "validate_document"
- request_type = UiCodeDocumentValidationRequest
- result_type = UiCodeDocumentValidationResult
-
- @classmethod
- def failed(cls, request, errors):
- return UiCodeDocumentValidationResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- valid=False,
- errors=errors,
- )
-
-
-class UiBridgeApplyDocumentOperation(UiCodeDocumentOperation):
- name = "apply_document"
- request_type = UiCodeDocumentApplyRequest
- result_type = UiCodeDocumentApplyResult
-
- @classmethod
- def failed(cls, request, errors):
- return UiCodeDocumentApplyResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- applied=False,
- base_revision_token=request.base_revision_token,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=errors,
- )
-
- @classmethod
- def accepted(cls, request, bridge_operation):
- return UiCodeDocumentApplyResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- applied=False,
- base_revision_token=request.base_revision_token,
- outcome=OperationRunning.name,
- operation_id=bridge_operation.identity.operation_id,
- receipt=UiMutationReceipt.accepted_for(
- request.request_token,
- bridge_operation_id=bridge_operation.identity.operation_id,
- ),
- )
-
- @classmethod
- def outcome(cls, result) -> str:
- return result.outcome
-
-
-class UiBridgeListSnapshotsOperation(UiObjectStateSnapshotOperation):
- name = "list_snapshots"
- request_type = UiSnapshotListRequest
- result_type = UiSnapshotCatalog
-
- @classmethod
- def failed(cls, request, errors):
- del request
- return UiSnapshotCatalog(
- schema_version=SCHEMA_VERSION,
- current_branch="",
- current_snapshot_index=-1,
- object_state_token=0,
- active=False,
- snapshots=(),
- branches=(),
- errors=errors,
- )
-
-
-class UiBridgeRestoreSnapshotOperation(
- UiObjectStateRestoreOperation, UiObjectStateSnapshotOperation
-):
- name = "restore_snapshot"
- request_type = UiSnapshotRestoreRequest
-
- @classmethod
- def prepare(cls, service, request):
- del service
- selectors = (request.snapshot_id, request.index, request.branch)
- if sum(selector is not None for selector in selectors) != 1:
- raise UiBridgeRequestRejected(
- (
- AgentError(
- code="invalid_snapshot_restore_request",
- message="Exactly one snapshot restore selector is required.",
- ),
- )
- )
- return request
-
-
-class UiBridgeTimeTravelHeadOperation(
- UiObjectStateRestoreOperation, UiObjectStateSnapshotOperation
-):
- name = "time_travel_head"
- request_type = UiTimeTravelHeadRequest
-
-
-class UiObjectStateBranchOperation(UiBridgeOperation):
- bridge_feature = "objectstate_branches"
-
-
-class UiBridgeListBranchesOperation(
- UiBridgeRequestlessOperation, UiObjectStateBranchOperation
-):
- name = "list_branches"
- result_type = UiBranchCatalog
-
- @classmethod
- def failed(cls, request, errors):
- return UiBranchCatalog(
- SCHEMA_VERSION, current_branch="", branches=(), errors=errors
- )
-
-
-class UiBridgeSwitchBranchOperation(
- UiObjectStateRestoreOperation, UiObjectStateBranchOperation
-):
- name = "switch_branch"
- request_type = UiBranchSwitchRequest
-
-
-class UiBridgeDescriptorDirectoryAuthority:
- """Filesystem location policy for live UI bridge descriptors."""
-
- @staticmethod
- def default_descriptor_dir() -> Path:
- return UiBridgeDescriptorDirectoryAuthority.descriptor_dirs()[0]
-
- @classmethod
- def descriptor_dirs(cls) -> tuple[Path, ...]:
- configured = environ.get(
- UiBridgeDescriptorEnvironment.descriptor_directory_path_key
- )
- if configured:
- return (AgentRuntimePlatform.resolved_path(configured),)
-
- return AgentRuntimePlatform.current().application_runtime_dirs(
- "OpenHCS",
- "ui-bridge",
- )
-
-
-class UiBridgeGatewayABC(ABC):
- """Transport to a running OpenHCS UI bridge, generic over the operation."""
-
- @abstractmethod
- def invoke(
- self,
- connection: UiBridgeConnectionSpec,
- operation: type[UiBridgeOperation],
- request,
- ):
- raise NotImplementedError
-
-
-
-class UiBridgeGatewayErrorABC(ABC):
- """Gateway-originated bridge failure that knows its agent-facing errors."""
-
- @abstractmethod
- def agent_errors(self, fallback_code: str) -> tuple[AgentError, ...]:
- raise NotImplementedError
-
-
-@dataclass(slots=True)
-class UiBridgeGatewayUnavailableError(ConnectionError, UiBridgeGatewayErrorABC):
- def __str__(self) -> str:
- return "No running OpenHCS UI bridge gateway is configured."
-
- def agent_errors(self, fallback_code: str) -> tuple[AgentError, ...]:
- del fallback_code
- return (
- AgentError.from_exception(
- "ui_bridge_unavailable",
- self,
- hint=self.discovery_hint(),
- ),
- )
-
- @staticmethod
- def discovery_hint() -> str:
- searched_dirs = ", ".join(
- str(path) for path in UiBridgeDescriptorDirectoryAuthority.descriptor_dirs()
- )
- return (
- "Pass descriptor_file_path, set OPENHCS_UI_BRIDGE_DESCRIPTOR, set "
- "OPENHCS_UI_BRIDGE_DESCRIPTOR_DIR, or restart the UI so its bridge "
- f"descriptor is written to one of the searched directories: {searched_dirs}."
- )
-
-
-@dataclass(slots=True)
-class UiBridgeGatewayResponseError(RuntimeError, UiBridgeGatewayErrorABC):
- errors: tuple[AgentError, ...]
-
- def __str__(self) -> str:
- if not self.errors:
- return "UI bridge returned an error response."
- return "; ".join(error.message for error in self.errors)
-
- def agent_errors(self, fallback_code: str) -> tuple[AgentError, ...]:
- del fallback_code
- return tuple(self._with_restart_hint(error) for error in self.errors)
-
- @staticmethod
- def _with_restart_hint(error: AgentError) -> AgentError:
- if error.code != "unsupported_ui_bridge_operation":
- return error
- if error.hint:
- return error
- return replace(
- error,
- hint=(
- "The running OpenHCS UI bridge does not expose this operation. "
- "Restart the UI or UI bridge process so it imports the current "
- "OpenHCS source, then retry the MCP call."
- ),
- )
-
-
-@dataclass(slots=True)
-class UiBridgeGatewayTimeoutError(TimeoutError, UiBridgeGatewayErrorABC):
- agent_error_code: ClassVar[str] = "ui_bridge_timeout"
-
- operation: str
- timeout_ms: int
-
- def __str__(self) -> str:
- return (
- f"UI bridge operation {self.operation!r} timed out after "
- f"{self.timeout_ms}ms."
- )
-
- def agent_errors(self, fallback_code: str) -> tuple[AgentError, ...]:
- del fallback_code
- return (
- AgentError(
- code=self.agent_error_code,
- message=str(self),
- hint=(
- "The running UI may be blocked or busy; retry after the UI "
- "event loop is responsive."
- ),
- exception_type=type(self).__name__,
- ),
- )
-
-
-def ui_bridge_gateway_errors(
- exception: Exception,
- fallback_code: str,
-) -> tuple[AgentError, ...]:
- """Project a gateway exception into agent-facing errors."""
-
- if isinstance(exception, UiBridgeGatewayErrorABC):
- return exception.agent_errors(fallback_code)
- return (AgentError.from_exception(fallback_code, exception),)
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeDescriptorResolution:
- status: str | None = None
- summaries: tuple[UiBridgeDescriptorSummary, ...] = ()
-
- def project_status(
- self,
- status_result: UiBridgeStatus,
- *,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeStatus:
- descriptor_status = status_result.descriptor_status
- if self.status is not None:
- descriptor_status = self.status
- descriptors = status_result.descriptors
- if self.summaries:
- descriptors = self.summaries
- return replace(
- status_result,
- connection=connection.public_connection(),
- descriptor_status=descriptor_status,
- descriptors=descriptors,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeConnectionResolution(UiBridgeConnectionSpec):
- descriptor: UiBridgeDescriptorResolution = UiBridgeDescriptorResolution()
- errors: tuple[AgentError, ...] = ()
-
- @classmethod
- def from_connection(
- cls,
- connection: UiBridgeConnectionSpec,
- *,
- descriptor: UiBridgeDescriptorResolution = UiBridgeDescriptorResolution(),
- errors: tuple[AgentError, ...] = (),
- ) -> "UiBridgeConnectionResolution":
- return project_dataclass(
- cls,
- connection,
- descriptor=descriptor,
- errors=errors,
- )
-
- @property
- def ok(self) -> bool:
- return not self.errors
-
- @property
- def process_id(self) -> int | None:
- """Return the sole validated UI process identity, when descriptor-backed."""
- if len(self.descriptor.summaries) != 1:
- return None
- return self.descriptor.summaries[0].pid
-
- def validate_live_status(
- self,
- status_result: UiBridgeStatus,
- ) -> tuple[AgentError, ...]:
- """Validate that a status response still belongs to the resolved descriptor."""
- if len(self.descriptor.summaries) != 1:
- return ()
- expected = self.descriptor.summaries[0]
- if expected.descriptor_file_path is None:
- return self._endpoint_identity_errors(
- "The resolved UI bridge descriptor has no descriptor-file identity."
- )
- reread = UiBridgeDescriptorReader.read(Path(expected.descriptor_file_path))
- if not reread.ok or reread.descriptor is None:
- return reread.errors
- current = reread.descriptor.public_summary(expected.status)
- if current != expected:
- return self._endpoint_identity_errors(
- "The UI bridge descriptor changed while its status was being checked."
- )
- if UiBridgeEndpointIdentity.from_value(
- status_result
- ) != UiBridgeEndpointIdentity.from_value(expected):
- return self._endpoint_identity_errors(
- "The responding UI bridge does not own the resolved descriptor endpoint."
- )
- return ()
-
- @staticmethod
- def _endpoint_identity_errors(message: str) -> tuple[AgentError, ...]:
- return (
- AgentError(
- code="ui_bridge_endpoint_identity_mismatch",
- message=message,
- hint="Resolve the live UI bridge again before retrying.",
- ),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeDescriptorReadResult:
- descriptor: UiBridgeDescriptorFile | None
- path: Path
- errors: tuple[AgentError, ...] = ()
- stale_process_descriptor: bool = False
-
- @property
- def ok(self) -> bool:
- return self.descriptor is not None and not self.errors
-
-
-@dataclass(slots=True)
-class UiBridgeDescriptorProcessGoneError(ValueError):
- pid: int
-
- def __str__(self) -> str:
- return f"UI bridge process is not running: {self.pid}"
-
-
-@dataclass(slots=True)
-class UiBridgeDescriptorProcessIdentityError(ValueError):
- pid: int
- descriptor_started_at_unix: float
- process_started_at_unix: float
-
- def __str__(self) -> str:
- return (
- f"UI bridge descriptor process identity is stale for PID {self.pid}: "
- f"the current process started at {self.process_started_at_unix}, "
- f"after the descriptor timestamp {self.descriptor_started_at_unix}"
- )
-
-
-@dataclass(frozen=True, slots=True)
-class LiveUiBridgeDescriptorSet:
- descriptors: tuple[UiBridgeDescriptorFile, ...]
-
- def resolution_runner(self) -> "DescriptorSetResolutionRunner":
- """Return the resolver for this set's size: none, one or many."""
- count = len(self.descriptors)
- if count == 0:
- return NoDescriptorSetResolutionRunner()
- if count == 1:
- return SingleDescriptorSetResolutionRunner()
- return AmbiguousDescriptorSetResolutionRunner()
-
- def only_descriptor(self) -> UiBridgeDescriptorFile:
- if len(self.descriptors) != 1:
- raise ValueError(
- "Live UI bridge descriptor set does not contain exactly one descriptor."
- )
- return self.descriptors[0]
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeEnvironment(UiBridgeConnectionFields):
- host: Annotated[
- NonBlankString | None,
- EnvironmentVariable("OPENHCS_UI_BRIDGE_HOST"),
- ] = None
- port: Annotated[
- SocketPort | None,
- EnvironmentVariable("OPENHCS_UI_BRIDGE_PORT"),
- ] = None
- transport_mode: Annotated[
- TransportMode | None,
- EnvironmentVariable("OPENHCS_UI_BRIDGE_TRANSPORT_MODE"),
- ] = None
- timeout_ms: Annotated[
- PositiveInteger | None,
- EnvironmentVariable("OPENHCS_UI_BRIDGE_TIMEOUT_MS"),
- ] = None
- auth_token: Annotated[
- NonBlankString | None,
- EnvironmentVariable("OPENHCS_UI_BRIDGE_AUTH_TOKEN"),
- ] = None
-
- @classmethod
- def current(cls) -> "UiBridgeEnvironment":
- return overlay_dataclass_from_environment(cls())
-
- def apply(self, connection: UiBridgeConnectionSpec) -> UiBridgeConnectionSpec:
- return UiBridgeConnectionSpec.from_fields(
- self,
- defaults=connection,
- )
-
-
-class DescriptorSetResolutionRunner(ABC):
- """Resolver behavior for a live UI bridge descriptor set of one size."""
-
- @abstractmethod
- def resolve(
- self,
- resolver: "UiBridgeDescriptorResolver",
- descriptor_set: LiveUiBridgeDescriptorSet,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- raise NotImplementedError
-
-
-class NoDescriptorSetResolutionRunner(DescriptorSetResolutionRunner):
- def resolve(
- self,
- resolver: "UiBridgeDescriptorResolver",
- descriptor_set: LiveUiBridgeDescriptorSet,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- del resolver, descriptor_set
- return UiBridgeConnectionResolution.from_connection(
- UiBridgeEnvironment.current().apply(connection),
- descriptor=UiBridgeDescriptorResolution(),
- )
-
-
-class SingleDescriptorSetResolutionRunner(DescriptorSetResolutionRunner):
- def resolve(
- self,
- resolver: "UiBridgeDescriptorResolver",
- descriptor_set: LiveUiBridgeDescriptorSet,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- return resolver._connection_from_descriptor(
- descriptor_set.only_descriptor(),
- connection,
- "ok",
- )
-
-
-class AmbiguousDescriptorSetResolutionRunner(DescriptorSetResolutionRunner):
- def resolve(
- self,
- resolver: "UiBridgeDescriptorResolver",
- descriptor_set: LiveUiBridgeDescriptorSet,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- return UiBridgeConnectionResolution.from_connection(
- connection,
- descriptor=UiBridgeDescriptorResolution(
- status="ambiguous_ui_bridge",
- summaries=tuple(
- descriptor.public_summary("live")
- for descriptor in descriptor_set.descriptors
- ),
- ),
- errors=(
- AgentError(
- code="ambiguous_ui_bridge",
- message="Multiple running OpenHCS UI bridge descriptors were found.",
- hint="Provide descriptor_file_path or bridge_instance_id.",
- ),
- ),
- )
-
-
-class UiBridgeDescriptorReader:
- """Read, parse, and validate one UI bridge descriptor file."""
-
- @classmethod
- def read(cls, path: Path) -> UiBridgeDescriptorReadResult:
- resolved_path = path.expanduser().resolve(strict=False)
- try:
- cls._validate_descriptor_file_path(resolved_path)
- payload = json.loads(resolved_path.read_text(encoding="utf-8"))
- if not isinstance(payload, dict):
- raise ValueError("UI bridge descriptor must be a JSON object.")
- descriptor = cls._descriptor_from_payload(
- dataclass_from_mapping(UiBridgeDescriptorWirePayload, payload),
- resolved_path,
- )
- cls._validate_descriptor_process(descriptor)
- cls._validate_descriptor_compatibility(descriptor)
- except Exception as exc:
- return UiBridgeDescriptorReadResult(
- descriptor=None,
- path=resolved_path,
- errors=(AgentError.from_exception("stale_ui_bridge_descriptor", exc),),
- stale_process_descriptor=isinstance(
- exc,
- (
- UiBridgeDescriptorProcessGoneError,
- UiBridgeDescriptorProcessIdentityError,
- ),
- ),
- )
- return UiBridgeDescriptorReadResult(descriptor=descriptor, path=resolved_path)
-
- @classmethod
- def _descriptor_from_payload(
- cls,
- descriptor_payload: UiBridgeDescriptorWirePayload,
- descriptor_path: Path,
- ) -> UiBridgeDescriptorFile:
- del cls
- return project_dataclass(
- UiBridgeDescriptorFile,
- descriptor_payload,
- descriptor_file_path=str(descriptor_path),
- )
-
- @staticmethod
- def _validate_descriptor_compatibility(
- descriptor: UiBridgeDescriptorFile,
- ) -> None:
- if descriptor.bridge_protocol_version != UI_BRIDGE_PROTOCOL_VERSION:
- raise ValueError(
- "Unsupported UI bridge protocol version: "
- f"{descriptor.bridge_protocol_version}"
- )
- OPENHCS_ENDPOINT_APPLICATION.compatibility_with(
- descriptor.application
- ).require_match()
-
- @staticmethod
- def _validate_descriptor_file_path(path: Path) -> None:
- stat_result = path.stat()
- if not stat.S_ISREG(stat_result.st_mode):
- raise PermissionError("UI bridge descriptor must be a regular file.")
- platform_authority = AgentRuntimePlatform.current()
- if not platform_authority.supports_posix_permissions():
- return
- uid = platform_authority.current_user_id()
- if uid is None:
- return
- if stat_result.st_uid != uid:
- raise PermissionError(
- "UI bridge descriptor is not owned by the current user."
- )
- if stat_result.st_mode & (stat.S_IRWXG | stat.S_IRWXO):
- raise PermissionError(
- "UI bridge descriptor must not be group/world accessible."
- )
- parent_stat = path.parent.stat()
- parent_mode = parent_stat.st_mode
- parent_is_sticky = bool(parent_mode & stat.S_ISVTX)
- if parent_mode & (stat.S_IWGRP | stat.S_IWOTH) and not parent_is_sticky:
- raise PermissionError(
- "UI bridge descriptor parent directory is writable by other users."
- )
-
- @staticmethod
- def _validate_descriptor_process(descriptor: UiBridgeDescriptorFile) -> None:
- process_started_at_unix = AgentRuntimePlatform.process_started_at_unix(
- descriptor.pid
- )
- if process_started_at_unix is None:
- raise UiBridgeDescriptorProcessGoneError(descriptor.pid)
- if process_started_at_unix > descriptor.started_at_unix:
- raise UiBridgeDescriptorProcessIdentityError(
- pid=descriptor.pid,
- descriptor_started_at_unix=descriptor.started_at_unix,
- process_started_at_unix=process_started_at_unix,
- )
-
-
-class UiBridgeDescriptorCatalog:
- """Discover the descriptor set selected for the current process."""
-
- @staticmethod
- def selected_descriptor_path() -> Path | None:
- """Return the exact descriptor selected by the shared environment contract."""
-
- configured = environ.get(UiBridgeDescriptorEnvironment.descriptor_file_path_key)
- if not configured:
- return None
- return AgentRuntimePlatform.resolved_path(configured)
-
- @classmethod
- def live_descriptors(cls) -> tuple[UiBridgeDescriptorFile, ...]:
- descriptors: list[UiBridgeDescriptorFile] = []
- for result in cls._read_descriptor_results():
- if result.ok and result.descriptor is not None:
- descriptors.append(result.descriptor)
- return tuple(descriptors)
-
- @classmethod
- def descriptor_catalog(cls) -> UiBridgeCatalog:
- descriptors: list[UiBridgeDescriptorSummary] = []
- errors: list[AgentError] = []
- for result in cls._read_descriptor_results():
- if result.descriptor is not None and not result.errors:
- descriptors.append(result.descriptor.public_summary("live"))
- continue
- errors.extend(result.errors)
- return UiBridgeCatalog(
- schema_version=SCHEMA_VERSION,
- bridges=tuple(descriptors),
- errors=tuple(errors),
- )
-
- @classmethod
- def _read_descriptor_results(cls) -> tuple[UiBridgeDescriptorReadResult, ...]:
- selected_path = cls.selected_descriptor_path()
- if selected_path is not None:
- return (UiBridgeDescriptorReader.read(selected_path),)
-
- descriptor_paths: list[Path] = []
- results: list[UiBridgeDescriptorReadResult] = []
- for directory in UiBridgeDescriptorDirectoryAuthority.descriptor_dirs():
- if not directory.exists():
- continue
- for path in sorted(directory.glob("ui_bridge_*.json")):
- descriptor_paths.append(path)
- descriptor_paths.extend(
- UiBridgeProcessAdvertisedDescriptorCatalog.descriptor_paths()
- )
-
- for path in dict.fromkeys(
- AgentRuntimePlatform.resolved_path(path)
- for path in descriptor_paths
- ):
- result = UiBridgeDescriptorReader.read(path)
- if result.stale_process_descriptor:
- cls._remove_stale_process_descriptor(result.path)
- continue
- results.append(result)
- return tuple(results)
-
- @staticmethod
- def _remove_stale_process_descriptor(path: Path) -> None:
- try:
- path.unlink()
- except OSError:
- return
-
-
-class UiBridgeProcessAdvertisedDescriptorCatalog:
- """Find descriptor files explicitly advertised by running local UI processes."""
-
- proc_root = Path("/proc")
- descriptor_environment_name = UiBridgeDescriptorEnvironment.descriptor_file_path_key
-
- @classmethod
- def descriptor_paths(cls) -> tuple[Path, ...]:
- paths: list[Path] = []
- for process_dir in cls._process_dirs():
- descriptor_path = cls._descriptor_path_from_process(process_dir)
- if descriptor_path is not None:
- paths.append(descriptor_path)
- return tuple(dict.fromkeys(paths))
-
- @classmethod
- def _process_dirs(cls) -> tuple[Path, ...]:
- try:
- return tuple(
- path for path in cls.proc_root.iterdir() if path.name.isdigit()
- )
- except (FileNotFoundError, PermissionError, OSError):
- return ()
-
- @classmethod
- def _descriptor_path_from_process(cls, process_dir: Path) -> Path | None:
- try:
- environment_payload = (process_dir / "environ").read_bytes()
- except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
- return None
- return cls._descriptor_path_from_environment(environment_payload)
-
- @classmethod
- def _descriptor_path_from_environment(cls, payload: bytes) -> Path | None:
- prefix = f"{cls.descriptor_environment_name}=".encode()
- for entry in payload.split(b"\0"):
- if not entry.startswith(prefix):
- continue
- value = entry.removeprefix(prefix)
- if not value:
- return None
- return Path(os.fsdecode(value))
- return None
-
-
-class UiBridgeDescriptorResolver:
- """Resolve UI bridge descriptors without widening the general path policy."""
-
- def resolve(
- self,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- if connection.descriptor_file_path is not None:
- return self._resolve_explicit_file(
- AgentRuntimePlatform.resolved_path(
- connection.descriptor_file_path
- ),
- connection,
- )
-
- selected_descriptor_path = UiBridgeDescriptorCatalog.selected_descriptor_path()
- if selected_descriptor_path is not None:
- return self._resolve_explicit_file(
- selected_descriptor_path,
- connection,
- )
-
- if connection.bridge_instance_id is not None:
- return self._resolve_instance(connection.bridge_instance_id, connection)
-
- descriptor_set = LiveUiBridgeDescriptorSet(
- UiBridgeDescriptorCatalog.live_descriptors()
- )
- return descriptor_set.resolution_runner().resolve(
- self, descriptor_set, connection
- )
-
- def _resolve_explicit_file(
- self,
- path: Path,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- result = UiBridgeDescriptorReader.read(path)
- if not result.ok or result.descriptor is None:
- return UiBridgeConnectionResolution.from_connection(
- replace(connection, descriptor_file_path=str(result.path)),
- descriptor=UiBridgeDescriptorResolution(
- status="stale_ui_bridge_descriptor",
- ),
- errors=result.errors,
- )
- return self._connection_from_descriptor(result.descriptor, connection, "ok")
-
- def _resolve_instance(
- self,
- bridge_instance_id: str,
- connection: UiBridgeConnectionSpec,
- ) -> UiBridgeConnectionResolution:
- live_descriptors = UiBridgeDescriptorCatalog.live_descriptors()
- matches = tuple(
- descriptor
- for descriptor in live_descriptors
- if descriptor.bridge_instance_id == bridge_instance_id
- )
- if not matches:
- return UiBridgeConnectionResolution.from_connection(
- connection,
- descriptor=UiBridgeDescriptorResolution(
- status="ui_bridge_descriptor_not_found",
- summaries=tuple(
- descriptor.public_summary("live")
- for descriptor in live_descriptors
- ),
- ),
- errors=(
- AgentError(
- code="ui_bridge_descriptor_not_found",
- message=f"No live OpenHCS UI bridge descriptor matches {bridge_instance_id!r}.",
- hint=(
- "Use one of the returned descriptors' bridge_instance_id "
- "values, pass descriptor_file_path, or omit both when "
- "exactly one live bridge is available."
- ),
- ),
- ),
- )
- return self._connection_from_descriptor(matches[0], connection, "ok")
-
- def _connection_from_descriptor(
- self,
- descriptor: UiBridgeDescriptorFile,
- connection: UiBridgeConnectionSpec,
- status: str,
- ) -> UiBridgeConnectionResolution:
- return UiBridgeConnectionResolution.from_connection(
- descriptor.resolve_connection(connection),
- descriptor=UiBridgeDescriptorResolution(
- status=status,
- summaries=(descriptor.public_summary(status),),
- ),
- )
-
-
-DEFAULT_UI_BRIDGE_DESCRIPTOR_RESOLVER = UiBridgeDescriptorResolver()
-
-
-class UiBridgeService:
- """Run UI-bridge operations against the running UI an agent names."""
-
- def __init__(
- self,
- gateway: UiBridgeGatewayABC | None = None,
- descriptor_resolver: UiBridgeDescriptorResolver = DEFAULT_UI_BRIDGE_DESCRIPTOR_RESOLVER,
- path_policy: AgentPathPolicy | None = None,
- ) -> None:
- if gateway is None:
- from openhcs.agent.services.ui_bridge_transport import ZMQUiBridgeGateway
-
- gateway = ZMQUiBridgeGateway()
- self.gateway = gateway
- self._descriptor_resolver = descriptor_resolver
- self.path_policy = path_policy or AgentPathPolicy.from_environment()
-
- def invoke(
- self,
- operation: type[UiBridgeOperation],
- request=None,
- connection: UiBridgeConnectionSpec = DEFAULT_UI_BRIDGE_CONNECTION_SPEC,
- ):
- """Run one operation; failures come back as the operation's own result."""
- return operation.respond(self, connection, request)
-
- def resolve(self, connection: UiBridgeConnectionSpec) -> UiBridgeConnectionResolution:
- return self._descriptor_resolver.resolve(connection)
-
- def connection_from_args(
- self,
- *,
- host: str | None = None,
- port: int | None = None,
- transport_mode: TransportMode | None = None,
- timeout_ms: int | None = None,
- auth_token: str | None = None,
- descriptor_file_path: str | None = None,
- bridge_instance_id: str | None = None,
- persistent: bool | None = None,
- ) -> UiBridgeConnectionSpec:
- return self.connection_from_fields(
- UiBridgeConnectionFields(
- host=host,
- port=port,
- transport_mode=transport_mode,
- persistent=persistent,
- timeout_ms=timeout_ms,
- auth_token=auth_token,
- descriptor_file_path=descriptor_file_path,
- bridge_instance_id=bridge_instance_id,
- )
- )
-
- def connection_from_fields(
- self,
- fields: UiBridgeConnectionFields,
- ) -> UiBridgeConnectionSpec:
- return UiBridgeConnectionSpec.from_fields(
- fields,
- defaults=DEFAULT_UI_BRIDGE_CONNECTION_SPEC,
- )
- def list_bridges(self) -> UiBridgeCatalog:
- return UiBridgeDescriptorCatalog.descriptor_catalog()
+class OpenHCSUiBridgeService(UiBridgeService):
+ """The UI-bridge client of OpenHCS agents, which also launches viewers beside the UI."""
def viewer_launch_context(
self,
connection: UiBridgeConnectionSpec = DEFAULT_UI_BRIDGE_CONNECTION_SPEC,
) -> ViewerLaunchContext:
- """Resolve one typed detached-viewer context from UI or current process."""
+ """The running UI's graphical session when its bridge resolves, else this process's."""
platform_authority = AgentRuntimePlatform.current()
- resolution = self._descriptor_resolver.resolve(connection)
+ resolution = self.resolve(connection)
if resolution.ok and resolution.process_id is not None:
environment = platform_authority.graphical_process_environment(
resolution.process_id,
diff --git a/openhcs/agent/services/ui_bridge_transport.py b/openhcs/agent/services/ui_bridge_transport.py
deleted file mode 100644
index d0c0b18404..0000000000
--- a/openhcs/agent/services/ui_bridge_transport.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""ZMQ transport for the OpenHCS running-UI bridge."""
-
-from __future__ import annotations
-
-import uuid
-from collections.abc import Mapping
-
-from python_introspect import JsonObject, dataclass_from_mapping, to_jsonable
-
-from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
- UiBridgeConnectionSpec,
- UiBridgeRequestEnvelope,
- UiBridgeResponseEnvelope,
-)
-from openhcs.agent.services.ui_bridge_service import (
- DEFAULT_UI_BRIDGE_TIMEOUT_MS,
- UI_BRIDGE_PROTOCOL_VERSION,
- UiBridgeGatewayABC,
- UiBridgeGatewayResponseError,
- UiBridgeGatewayTimeoutError,
- UiBridgeGatewayUnavailableError,
- UiBridgeOperation,
-)
-from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
-from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG
-
-UI_BRIDGE_CONTROL_TIMEOUT_MAX_MS = DEFAULT_UI_BRIDGE_TIMEOUT_MS
-UI_BRIDGE_CONTROL_TIMEOUT_MIN_MS = 1
-
-
-class UiBridgeControlClient:
- """Small JSON/ZMQ client for the UI bridge control socket."""
-
- def request(
- self,
- connection: UiBridgeConnectionSpec,
- operation: type[UiBridgeOperation],
- request=None,
- ) -> JsonObject:
- if connection.port is None:
- raise UiBridgeGatewayUnavailableError
- operation.validate_request(request)
-
- request = UiBridgeRequestEnvelope(
- schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
- application=OPENHCS_ENDPOINT_APPLICATION,
- request_id=str(uuid.uuid4()),
- operation=operation.require_name(),
- auth_token=connection.auth_token,
- payload=self._payload_object(request),
- )
- response_payload = self._send(connection, to_jsonable(request))
- response = dataclass_from_mapping(
- UiBridgeResponseEnvelope,
- response_payload,
- )
- self._validate_response(request, response)
- if not response.ok:
- raise UiBridgeGatewayResponseError(response.errors)
- return response.payload
-
- def _send(
- self,
- connection: UiBridgeConnectionSpec,
- request_payload: JsonObject,
- ) -> JsonObject:
- import zmq
-
- context = zmq.Context.instance()
- socket = context.socket(zmq.REQ)
- timeout_ms = self._socket_timeout_ms(connection)
- request_operation = self._request_operation(request_payload)
- socket.setsockopt(zmq.LINGER, 0)
- socket.setsockopt(zmq.RCVTIMEO, timeout_ms)
- socket.setsockopt(zmq.SNDTIMEO, timeout_ms)
- try:
- socket.connect(connection.zmq_data_url(OPENHCS_ZMQ_CONFIG))
- socket.send_json(request_payload)
- response = socket.recv_json()
- except zmq.Again as exc:
- raise UiBridgeGatewayTimeoutError(
- operation=request_operation,
- timeout_ms=timeout_ms,
- ) from exc
- finally:
- socket.close(linger=0)
- if not isinstance(response, Mapping):
- raise TypeError(
- f"UI bridge response must be a JSON object, got {type(response).__name__}"
- )
- return dict(response)
-
- @staticmethod
- def _socket_timeout_ms(connection: UiBridgeConnectionSpec) -> int:
- return min(
- max(connection.timeout_ms, UI_BRIDGE_CONTROL_TIMEOUT_MIN_MS),
- UI_BRIDGE_CONTROL_TIMEOUT_MAX_MS,
- )
-
- @staticmethod
- def _request_operation(request_payload: JsonObject) -> str:
- if "operation" not in request_payload:
- raise ValueError(
- "UI bridge request payload missing required field 'operation'."
- )
- operation = request_payload["operation"]
- if not isinstance(operation, str):
- raise TypeError(
- "UI bridge request payload field 'operation' must be a string."
- )
- return operation
-
- @staticmethod
- def _payload_object(payload) -> JsonObject:
- if payload is None:
- return {}
- json_payload = to_jsonable(payload)
- if not isinstance(json_payload, Mapping):
- raise TypeError(
- f"UI bridge request payload must serialize to a JSON object, "
- f"got {type(json_payload).__name__}"
- )
- return json_payload
-
- @staticmethod
- def _validate_response(
- request: UiBridgeRequestEnvelope,
- response: UiBridgeResponseEnvelope,
- ) -> None:
- if response.schema_version != SCHEMA_VERSION:
- raise ValueError(
- f"Unsupported agent schema version: {response.schema_version}"
- )
- if response.bridge_protocol_version != UI_BRIDGE_PROTOCOL_VERSION:
- raise ValueError(
- f"Unsupported UI bridge protocol version: {response.bridge_protocol_version}"
- )
- OPENHCS_ENDPOINT_APPLICATION.compatibility_with(
- response.application
- ).require_match()
- if response.request_id != request.request_id:
- raise ValueError("UI bridge response request_id does not match request.")
-
-
-class ZMQUiBridgeGateway(UiBridgeGatewayABC):
- """Gateway that connects MCP/agent services to a running PyQt UI bridge."""
-
- def __init__(self, client: UiBridgeControlClient | None = None) -> None:
- if client is None:
- client = UiBridgeControlClient()
- self._client = client
-
- def invoke(
- self,
- connection: UiBridgeConnectionSpec,
- operation: type[UiBridgeOperation],
- request,
- ):
- return dataclass_from_mapping(
- operation.result_type,
- self._client.request(connection, operation, request),
- )
diff --git a/openhcs/agent/services/viewer_window_service.py b/openhcs/agent/services/viewer_window_service.py
index fdd33d70ba..8202eff428 100644
--- a/openhcs/agent/services/viewer_window_service.py
+++ b/openhcs/agent/services/viewer_window_service.py
@@ -20,10 +20,7 @@
)
from python_introspect.validation import validate_annotation_value
from polystore.streaming.identity import StreamProducerIdentity
-from pyqt_reactive.services.window_snapshot import (
- WindowSnapshotCaptureSpec,
- WindowVisualObservation,
-)
+from formbridge.services.window_snapshot import WindowSnapshotCaptureSpec, WindowVisualObservation
from zmqruntime.client import (
EndpointShutdownMode,
EndpointShutdownResult,
@@ -36,12 +33,8 @@
)
import openhcs.core.plate_image_inventory as core_plate_image_inventory
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
- AgentError,
- AgentResourceRef,
- AgentWarning,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentResourceRef, AgentWarning
from openhcs.agent.dto.execution import ExecutionConnectionSpec
from openhcs.agent.dto.viewer import (
ViewerWindowPolylineMeasurementRequest,
diff --git a/openhcs/agent/ui_bridge_application.py b/openhcs/agent/ui_bridge_application.py
new file mode 100644
index 0000000000..5ff7da12b6
--- /dev/null
+++ b/openhcs/agent/ui_bridge_application.py
@@ -0,0 +1,46 @@
+"""OpenHCS's declaration of its running-UI bridge."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from formbridge.bridge.application import UiBridgeApplication
+
+from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
+from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG
+
+
+class OpenHCSUiBridgeApplication(UiBridgeApplication):
+ protocol_version = "openhcs.ui_bridge.v4"
+ endpoint_application = OPENHCS_ENDPOINT_APPLICATION
+ transport_config = OPENHCS_ZMQ_CONFIG
+ runtime_name = "OpenHCS"
+ descriptor_file_variable = "OPENHCS_UI_BRIDGE_DESCRIPTOR"
+ descriptor_directory_variable = "OPENHCS_UI_BRIDGE_DESCRIPTOR_DIR"
+ server_name = "OpenHCSUiBridgeServer"
+ service_name = "openhcs.ui_bridge"
+
+ @classmethod
+ def window_snapshot_dir(cls) -> Path:
+ from openhcs.agent.path_policy import DEFAULT_AGENT_WINDOW_SNAPSHOT_DIR
+
+ return DEFAULT_AGENT_WINDOW_SNAPSHOT_DIR
+
+ @classmethod
+ def connection_environment(cls):
+ from openhcs.agent.services.ui_bridge_service import UiBridgeEnvironment
+
+ return UiBridgeEnvironment()
+
+ @classmethod
+ def output_path_policy(cls):
+ from openhcs.agent.path_policy import AgentPathPolicy
+
+ return AgentPathPolicy.from_environment()
+
+ @classmethod
+ def log_file_path(cls) -> str | None:
+ from openhcs.core.log_utils import get_current_log_file_path
+
+ return get_current_log_file_path()
+
diff --git a/openhcs/agent/ui_bridge_environment.py b/openhcs/agent/ui_bridge_environment.py
index 66d53414b7..da9c8541f5 100644
--- a/openhcs/agent/ui_bridge_environment.py
+++ b/openhcs/agent/ui_bridge_environment.py
@@ -38,18 +38,3 @@ def cache_file_path(
raise ValueError(f"{cls.cache_file_path_key} must be an absolute path.")
return cache_file.resolve(strict=False)
-
-class UiBridgeDescriptorEnvironment:
- """Own the environment selectors for one live UI bridge descriptor."""
-
- descriptor_file_path_key = "OPENHCS_UI_BRIDGE_DESCRIPTOR"
- descriptor_directory_path_key = "OPENHCS_UI_BRIDGE_DESCRIPTOR_DIR"
-
- @classmethod
- def child_process_environment_keys(cls) -> tuple[str, str]:
- """Return every selector required by a descriptor-consuming child."""
-
- return (
- cls.descriptor_file_path_key,
- cls.descriptor_directory_path_key,
- )
diff --git a/openhcs/agent/ui_bridge_identities.py b/openhcs/agent/ui_bridge_identities.py
index 30801f00df..f2eb163640 100644
--- a/openhcs/agent/ui_bridge_identities.py
+++ b/openhcs/agent/ui_bridge_identities.py
@@ -1,102 +1,14 @@
-"""Registered identities for stable agent-facing UI bridge objects."""
+"""OpenHCS's registered UI bridge identities: its windows, widgets, documents and surfaces."""
from __future__ import annotations
-from abc import ABC
-from typing import ClassVar
-
-from metaclass_registry import AutoRegisterMeta
-
-
-class UiBridgeIdentityDeclaration(ABC, metaclass=AutoRegisterMeta):
- """Registered identity for stable agent-facing UI bridge objects."""
-
- __registry_key__ = "value"
- __skip_if_no_key__ = True
-
- value: ClassVar[str | None] = None
- enum_member_name: ClassVar[str | None] = None
-
- @classmethod
- def require_value(cls) -> str:
- if cls.value is None:
- raise ValueError(
- f"{cls.__name__} does not declare a bridge identity value."
- )
- return cls.value
-
-
-class UiStableWindowIdentityDeclaration(UiBridgeIdentityDeclaration):
- """Registered identity and presentation for a stable public UI window."""
-
- title: ClassVar[str | None] = None
-
- @classmethod
- def require_title(cls) -> str:
- """Return the declaration-owned public window title."""
-
- if cls.title is None:
- raise ValueError(f"{cls.__name__} does not declare a window title.")
- return cls.title
-
- @classmethod
- def has_title(cls) -> bool:
- return cls.title is not None
-
- @classmethod
- def declaration_types(
- cls,
- ) -> tuple[type[UiStableWindowIdentityDeclaration], ...]:
- """Project the complete stable-window family from the identity registry."""
-
- return tuple(
- dict.fromkeys(
- declaration
- for declaration in cls.__registry__.values()
- if issubclass(declaration, cls)
- and declaration.value is not None
- and declaration.has_title()
- )
- )
-
-
-class UiWidgetIdentityDeclaration(UiBridgeIdentityDeclaration):
- """Registered widget identity declaration."""
-
- @classmethod
- def action_provider_id(cls) -> str:
- """Return the stable action-provider id for this widget."""
- return f"{cls.require_value()}.actions"
-
-
-class UiStableWidgetIdentityDeclaration(
- UiWidgetIdentityDeclaration,
+from formbridge.bridge.identities import (
+ UiCodeDocumentIdentityDeclaration,
+ UiStableWidgetIdentityDeclaration,
UiStableWindowIdentityDeclaration,
-):
- """Stable public window whose content is also a registered UI widget."""
-
-
-class UiOwnedByWidgetIdentityDeclaration(UiBridgeIdentityDeclaration):
- """Identity declaration for a UI bridge object owned by one widget."""
-
- widget_identity: ClassVar[type[UiWidgetIdentityDeclaration] | None] = None
-
- @classmethod
- def widget_id(cls) -> str:
- if cls.widget_identity is None:
- raise ValueError(f"{cls.__name__} does not declare a widget identity.")
- return cls.widget_identity.require_value()
-
-
-class UiCodeDocumentIdentityDeclaration(UiOwnedByWidgetIdentityDeclaration):
- """Registered code document identity declaration."""
-
-
-class UiStateSurfaceIdentityDeclarationBase(UiBridgeIdentityDeclaration):
- """Registered protocol identity for one UI state surface.
-
- Widget ownership and presentation metadata live on the owning widget class.
- """
+ UiStateSurfaceIdentityDeclarationBase,
+ UiWidgetIdentityDeclaration,
+)
class UiSessionViewWidgetIdentityDeclaration(UiStableWidgetIdentityDeclaration):
@@ -113,32 +25,23 @@ def has_title(cls) -> bool:
return True
-class PlateManagerWidgetIdentity(UiSessionViewWidgetIdentityDeclaration):
- value = "plate_manager"
- enum_member_name = "PLATE_MANAGER"
+class DatasetListWidgetIdentity(UiSessionViewWidgetIdentityDeclaration):
+ value = "datasets"
class PipelineEditorWidgetIdentity(UiSessionViewWidgetIdentityDeclaration):
value = "pipeline_editor"
- enum_member_name = "PIPELINE_EDITOR"
class PipelineDebugToolbarWidgetIdentity(UiWidgetIdentityDeclaration):
value = "pipeline_debug_toolbar"
- enum_member_name = "PIPELINE_DEBUG_TOOLBAR"
class MainWindowWidgetIdentity(UiStableWidgetIdentityDeclaration):
value = "main_window"
- enum_member_name = "MAIN_WINDOW"
title = "OpenHCS"
-class ManagedWindowWidgetIdentity(UiWidgetIdentityDeclaration):
- value = "managed_window"
- enum_member_name = "MANAGED_WINDOW"
-
-
class SystemMonitorWindowIdentity(UiStableWindowIdentityDeclaration):
value = "system_monitor"
title = "System Monitor"
@@ -162,6 +65,7 @@ class LogViewerWindowIdentity(UiStableWindowIdentityDeclaration):
class GlobalConfigWindowIdentity(UiStableWindowIdentityDeclaration):
value = "global_config"
title = "Configure OpenHCS"
+ manager_scope_alias = ""
class KnowledgeBaseWindowIdentity(UiStableWindowIdentityDeclaration):
@@ -174,42 +78,30 @@ class AboutOpenHCSWindowIdentity(UiStableWindowIdentityDeclaration):
title = "About OpenHCS"
-class PlateManagerOrchestratorCodeDocumentIdentity(UiCodeDocumentIdentityDeclaration):
- value = "plate_manager.orchestrator_config"
- enum_member_name = "PLATE_MANAGER_ORCHESTRATOR"
- widget_identity = PlateManagerWidgetIdentity
+class DatasetListCodeDocumentIdentity(UiCodeDocumentIdentityDeclaration):
+ value = "datasets.orchestrator_config"
+ widget_identity = DatasetListWidgetIdentity
-class PlateManagerStateSurfaceIdentityDeclaration(
+class DatasetListStateSurfaceIdentity(
UiStateSurfaceIdentityDeclarationBase
):
- value = "plate_manager.state"
- enum_member_name = "PLATE_MANAGER"
+ value = "datasets.state"
-class PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration(
+class DatasetLiveMeasurementsStateSurfaceIdentity(
UiStateSurfaceIdentityDeclarationBase
):
- value = "plate_manager.live_measurements"
- enum_member_name = "PLATE_MANAGER_LIVE_MEASUREMENTS"
+ value = "datasets.live_measurements"
class PipelineEditorStateSurfaceIdentityDeclaration(
UiStateSurfaceIdentityDeclarationBase
):
value = "pipeline_editor.state"
- enum_member_name = "PIPELINE_EDITOR"
class PipelineDebugSessionStateSurfaceIdentityDeclaration(
UiStateSurfaceIdentityDeclarationBase
):
value = "pipeline_debug_toolbar.session"
- enum_member_name = "PIPELINE_DEBUG_SESSION"
-
-
-class UiLiveOverviewStateSurfaceIdentityDeclaration(
- UiStateSurfaceIdentityDeclarationBase
-):
- value = "ui_live_overview.state"
- enum_member_name = "UI_LIVE_OVERVIEW"
diff --git a/openhcs/authoring/session/dataset_document.py b/openhcs/authoring/session/dataset_document.py
index 5d8483234f..05c9ad1feb 100644
--- a/openhcs/authoring/session/dataset_document.py
+++ b/openhcs/authoring/session/dataset_document.py
@@ -15,9 +15,9 @@
from objectstate.object_state import ObjectState, ObjectStateRegistry
from objectstate.value_semantics import semantic_values_equal
-from openhcs.core.selection import AllItems, SelectedItems, SelectionScope
+from formbridge.selection import AllItems, SelectedItems, SelectionScope
from openhcs.core.config import GlobalPipelineConfig, PipelineConfig
-from openhcs.core.selection import SelectedScopeIdsCarrier
+from formbridge.selection import SelectedScopeIdsCarrier
from objectstate import retain_only_authored_paths
from openhcs.ui.shared.plate_manager_code_document import (
PlateManagerCodeDocumentAuthority,
diff --git a/openhcs/authoring/session/datasets.py b/openhcs/authoring/session/datasets.py
index 1fe34b784c..2dc003653c 100644
--- a/openhcs/authoring/session/datasets.py
+++ b/openhcs/authoring/session/datasets.py
@@ -4,6 +4,7 @@
from dataclasses import dataclass
+from formbridge.bridge.scope_policy import HiddenObjectStateScope
from objectstate.collection_containers import RootState
from objectstate.object_state import ObjectState, ObjectStateRegistry
@@ -14,6 +15,12 @@
DATASET_LIST_PARAMETER = "orchestrator_scope_ids"
+class DatasetListScope(HiddenObjectStateScope):
+ """The dataset list root is a system scope; agents read each dataset's own scope."""
+
+ scope_id = DATASET_LIST_SCOPE_ID
+
+
@dataclass(frozen=True, slots=True)
class DatasetRow:
"""One visible dataset row."""
diff --git a/openhcs/authoring/session/operations/__init__.py b/openhcs/authoring/session/operations/__init__.py
index 4f103322e5..c6016a4ddb 100644
--- a/openhcs/authoring/session/operations/__init__.py
+++ b/openhcs/authoring/session/operations/__init__.py
@@ -14,7 +14,7 @@
from metaclass_registry import AutoRegisterMeta, LazyDiscoveryDict, RegistryConfig
-from openhcs.agent.dto.common import AgentError, AgentWarning
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.session import NoArgumentsRequest, SessionOperationResult
if TYPE_CHECKING:
diff --git a/openhcs/authoring/session/operations/application.py b/openhcs/authoring/session/operations/application.py
index 2e6b555557..447ccdf38f 100644
--- a/openhcs/authoring/session/operations/application.py
+++ b/openhcs/authoring/session/operations/application.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from openhcs.agent.dto.common import AgentWarning
+from formbridge.contracts import AgentWarning
from openhcs.authoring.session.operations import RendererOperation
diff --git a/openhcs/authoring/session/operations/datasets.py b/openhcs/authoring/session/operations/datasets.py
index 615072d580..ca267196bc 100644
--- a/openhcs/authoring/session/operations/datasets.py
+++ b/openhcs/authoring/session/operations/datasets.py
@@ -8,11 +8,12 @@
from zmqruntime.startup import EndpointStartupPresentationTarget
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec
from openhcs.agent.dto.session import (
DatasetPipelineSourceRequest,
DatasetRootsRequest,
+ DatasetRowState,
DatasetRunRequest,
DatasetTargetsRequest,
NoArgumentsRequest,
@@ -24,6 +25,13 @@
RendererOperation,
SessionOperation,
)
+from openhcs.constants.constants import (
+ OrchestratorCompileFailed,
+ OrchestratorExecFailed,
+ OrchestratorInitFailed,
+ OrchestratorState,
+)
+from openhcs.core.execution_state import TerminalExecutionStatus
if TYPE_CHECKING:
pass
@@ -104,9 +112,34 @@ def available(cls, session, request) -> AgentError | None:
class DatasetWorkflowOperation(SessionOperation):
- """One step of the dataset workflow (initialize, compile, run)."""
+ """One step of the dataset workflow (initialize, compile, run).
+
+ Each step also says when a dataset row shows it finished or failed.
+ """
workflow_name: ClassVar[str]
+ failed_orchestrator_states: ClassVar[tuple[type[OrchestratorState], ...]] = ()
+ terminal_state_is_idempotent: ClassVar[bool] = False
+ """A row already in the terminal state proves completion without a change."""
+
+ @classmethod
+ def terminal_for_row(cls, row: DatasetRowState) -> bool:
+ """Whether ``row`` shows this step finished."""
+ raise NotImplementedError(f"{cls.__name__} declares no terminal row state.")
+
+ @classmethod
+ def failed_for_row(cls, row: DatasetRowState) -> bool:
+ """Whether ``row`` shows this step failed."""
+ failed_terminal_statuses = {
+ name
+ for name, status in TerminalExecutionStatus.__registry__.items()
+ if status.counts_as_failed
+ }
+ failed_states = {state.name for state in cls.failed_orchestrator_states}
+ return (
+ row.terminal_status in failed_terminal_statuses
+ or row.orchestrator_state in failed_states
+ )
class AddDatasets(PromptedOperation, HeadlessOperation):
@@ -182,6 +215,8 @@ def available(cls, session, request) -> AgentError | None:
class InitializeDatasets(IdleTargetsOperation, DatasetWorkflowOperation, HeadlessOperation):
operation_id = "initialize_datasets"
workflow_name = "INIT"
+ failed_orchestrator_states = (OrchestratorInitFailed,)
+ terminal_state_is_idempotent = True
label = "Init"
tooltip = "Initialize selected datasets"
description = (
@@ -190,6 +225,10 @@ class InitializeDatasets(IdleTargetsOperation, DatasetWorkflowOperation, Headles
)
side_effects = ("writes_dataset_metadata",)
+ @classmethod
+ def terminal_for_row(cls, row: DatasetRowState) -> bool:
+ return row.init_pending is False and row.initialized is True
+
@classmethod
def run(cls, session, request) -> SessionOperationResult:
session.start(session.initialize_datasets, tuple(request.scope_ids))
@@ -221,6 +260,7 @@ class CompileDatasets(
):
operation_id = "compile_datasets"
workflow_name = "COMPILE"
+ failed_orchestrator_states = (OrchestratorCompileFailed,)
label = "Compile"
tooltip = "Compile dataset pipelines"
description = (
@@ -229,6 +269,10 @@ class CompileDatasets(
)
side_effects = ("submits_compile_jobs",)
+ @classmethod
+ def terminal_for_row(cls, row: DatasetRowState) -> bool:
+ return row.compile_pending is False and row.compiled is True
+
@classmethod
def resolved(cls, session) -> type[SessionOperation]:
slot = _CompileSlot()
@@ -307,6 +351,7 @@ def run(cls, session, request) -> SessionOperationResult:
class RunDatasets(DatasetTargetsOperation, DatasetWorkflowOperation, HeadlessOperation):
operation_id = "run_datasets"
workflow_name = "RUN"
+ failed_orchestrator_states = (OrchestratorExecFailed,)
label = "Run"
tooltip = "Run or stop dataset execution"
description = (
@@ -316,6 +361,10 @@ class RunDatasets(DatasetTargetsOperation, DatasetWorkflowOperation, HeadlessOpe
request = DatasetRunRequest
side_effects = ("submits_execution_jobs",)
+ @classmethod
+ def terminal_for_row(cls, row: DatasetRowState) -> bool:
+ return row.execution_active is False and row.queued is False and row.has_terminal_status
+
@classmethod
def resolved(cls, session) -> type[SessionOperation]:
return StopExecution if session.execution_state.busy else cls
diff --git a/openhcs/authoring/session/operations/pipelines.py b/openhcs/authoring/session/operations/pipelines.py
index 66dd730864..9a608fa868 100644
--- a/openhcs/authoring/session/operations/pipelines.py
+++ b/openhcs/authoring/session/operations/pipelines.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from openhcs.agent.dto.session import (
DatasetRequest,
PipelineFileRequest,
diff --git a/openhcs/authoring/session/pipelines.py b/openhcs/authoring/session/pipelines.py
index 397af7755e..2078828636 100644
--- a/openhcs/authoring/session/pipelines.py
+++ b/openhcs/authoring/session/pipelines.py
@@ -17,18 +17,13 @@
from openhcs.core.dataset_sources.dataset_scopes import DatasetScope
from openhcs.ui.shared.plate_scope_identity import PipelineScopeIdentity
from openhcs.authoring.session.step_scopes import FunctionStepScopeToken
-from pyqt_reactive.services.function_pattern_code_document import (
+from formbridge.services.function_pattern_code_document import (
FunctionPatternCodeDocumentService,
FunctionPatternValue,
function_pattern_authority,
)
-from pyqt_reactive.services.pattern_data_manager import (
- FUNC_EDITOR_PATTERN_TOKENS_META_KEY,
-)
-from pyqt_reactive.services.scope_token_service import (
- ScopeTokenService,
- reconcile_occurrence_tokens,
-)
+from formbridge.services.pattern_data_manager import FUNC_EDITOR_PATTERN_TOKENS_META_KEY
+from formbridge.services.scope_token_service import ScopeTokenService, reconcile_occurrence_tokens
PipelineFunctionPattern = FunctionSpec | None
FunctionPatternTokenTree = list[str] | dict[str, "FunctionPatternTokenTree"] | None
diff --git a/openhcs/authoring/session/progress.py b/openhcs/authoring/session/progress.py
index 360bd19e82..7462833bb7 100644
--- a/openhcs/authoring/session/progress.py
+++ b/openhcs/authoring/session/progress.py
@@ -11,14 +11,11 @@
import threading
from typing import TYPE_CHECKING
-from pyqt_reactive.services.interval_snapshot_poller import (
+from formbridge.services.interval_snapshot_poller import (
CallbackIntervalSnapshotPollerPolicy,
IntervalSnapshotPoller,
)
-from pyqt_reactive.services.zmq_server_info import (
- BaseServerInfo,
- ExecutionServerInfo,
-)
+from formbridge.services.zmq_server_info import BaseServerInfo, ExecutionServerInfo
from zmqruntime.progress import EventRegistryMutation
from openhcs.authoring.session.events import (
diff --git a/openhcs/authoring/session/session.py b/openhcs/authoring/session/session.py
index 90db36c345..cd495d063d 100644
--- a/openhcs/authoring/session/session.py
+++ b/openhcs/authoring/session/session.py
@@ -25,9 +25,9 @@
from objectstate.lazy_factory import ensure_global_config_context
from objectstate.object_state import ObjectState, ObjectStateRegistry
from polystore.base import _create_storage_registry
-from pyqt_reactive.services.async_operation_executor import AsyncOperationExecutor
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
-from pyqt_reactive.services.zmq_server_scan_service import EndpointObservationSnapshot
+from formbridge.services.async_operation_executor import AsyncOperationExecutor
+from formbridge.services.scope_token_service import ScopeTokenService
+from formbridge.services.zmq_server_scan_service import EndpointObservationSnapshot
from zmqruntime.messages import ExecutionRecord
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
@@ -152,7 +152,7 @@
)
if TYPE_CHECKING:
- from openhcs.agent.dto.common import AgentError
+ from formbridge.contracts import AgentError
from openhcs.authoring.session.operations import SessionOperation
from openhcs.authoring.session.views import SessionView
from zmqruntime.messages import PongResponse
diff --git a/openhcs/authoring/session/step_scopes.py b/openhcs/authoring/session/step_scopes.py
index 0c1dc9b3f9..0ccd046cea 100644
--- a/openhcs/authoring/session/step_scopes.py
+++ b/openhcs/authoring/session/step_scopes.py
@@ -7,12 +7,12 @@
from objectstate import ScopeKey
from openhcs.core.steps.function_step import FunctionStep
-from pyqt_reactive.services.function_navigation import (
+from formbridge.services.function_navigation import (
FUNCTION_FIELD_ROOT,
build_function_token_field_path,
is_function_field_path,
)
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.scope_token_service import ScopeTokenService
FUNCTION_STEP_SCOPE_TOKEN_PREFIX = f"{FunctionStep.__name__.lower()}_"
diff --git a/openhcs/authoring/session/views.py b/openhcs/authoring/session/views.py
index a736b56a43..26a967c52a 100644
--- a/openhcs/authoring/session/views.py
+++ b/openhcs/authoring/session/views.py
@@ -23,7 +23,7 @@
)
from openhcs.agent.ui_bridge_identities import (
PipelineEditorWidgetIdentity,
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
UiSessionViewWidgetIdentityDeclaration,
)
from openhcs.authoring.session.dataset_status import PlateStatusPresenter
@@ -232,7 +232,7 @@ def status_prefix(self) -> str:
class DatasetListView(SessionView):
view_id = "dataset_list"
title = "Datasets"
- window = PlateManagerWidgetIdentity
+ window = DatasetListWidgetIdentity
state = DatasetListState
operations = (
AddDatasets,
diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py
index ce8938142e..be0a0d8b30 100644
--- a/openhcs/core/function_patterns.py
+++ b/openhcs/core/function_patterns.py
@@ -27,7 +27,7 @@
from openhcs.core.steps.function_runtime import FunctionCoreExecutor
from arraybridge import MemoryType
-from pyqt_reactive.pattern_metadata import PatternScopeToken
+from formbridge.pattern_metadata import PatternScopeToken
from python_introspect import Enableable
from openhcs.core.artifacts.plans import (
diff --git a/openhcs/core/log_utils.py b/openhcs/core/log_utils.py
index a218212dd3..0a505d1ae3 100644
--- a/openhcs/core/log_utils.py
+++ b/openhcs/core/log_utils.py
@@ -10,7 +10,7 @@
from pathlib import Path
from typing import List, Optional
-from pyqt_reactive.core.log_utils import LogFileInfo, LogType
+from formbridge.core.log_utils import LogFileInfo, LogType
from zmqruntime.execution.logs import ExecutionWorkerLogIdentity
from zmqruntime.messages import ProcessIdentity
diff --git a/openhcs/core/selection.py b/openhcs/core/selection.py
deleted file mode 100644
index d05eb486a7..0000000000
--- a/openhcs/core/selection.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""Shared selection carriers for UI and agent-facing projections."""
-
-from __future__ import annotations
-
-from abc import ABC, abstractmethod
-from collections.abc import Iterable
-from dataclasses import dataclass
-from typing import ClassVar
-
-from metaclass_registry import AutoRegisterMeta
-
-
-class SelectionScope(ABC, metaclass=AutoRegisterMeta):
- """Whether a view or document covers the selected items or all of them.
-
- ``name`` is the scope's spelling on the wire and in CLI choices.
- """
-
- __registry_key__ = "name"
- __skip_if_no_key__ = True
-
- name: ClassVar[str | None] = None
-
- @staticmethod
- def named(name: str) -> type[SelectionScope]:
- scope = SelectionScope.__registry__.get(name)
- if scope is None:
- raise ValueError(
- f"Unknown selection mode {name!r}; expected one of "
- f"{tuple(SelectionScope.__registry__)!r}."
- )
- return scope
-
- @staticmethod
- @abstractmethod
- def keeps(selected: bool) -> bool:
- """Whether an item with this selection state is in scope."""
-
- @staticmethod
- @abstractmethod
- def pick(selected: tuple[str, ...], every: tuple[str, ...]) -> tuple[str, ...]:
- """The ids in scope, given the selected ids (in selection order) and all ids."""
-
-
-class SelectedItems(SelectionScope):
- name = "selected"
-
- @staticmethod
- def keeps(selected: bool) -> bool:
- return selected
-
- @staticmethod
- def pick(selected: tuple[str, ...], every: tuple[str, ...]) -> tuple[str, ...]:
- return selected
-
-
-class AllItems(SelectionScope):
- name = "all"
-
- @staticmethod
- def keeps(selected: bool) -> bool:
- return True
-
- @staticmethod
- def pick(selected: tuple[str, ...], every: tuple[str, ...]) -> tuple[str, ...]:
- return every
-
-
-@dataclass(frozen=True, kw_only=True, slots=True)
-class SelectionModeCarrier:
- selection_mode: str | None = None
-
- def resolved_selection_mode(self, default: type[SelectionScope]) -> str:
- """The requested mode's wire spelling, or ``default``'s."""
- if self.selection_mode is None:
- return default.name
- return SelectionScope.named(self.selection_mode).name
-
- def selection_scope(self, default: type[SelectionScope]) -> type[SelectionScope]:
- return SelectionScope.named(self.resolved_selection_mode(default))
-
-
-@dataclass(frozen=True, kw_only=True, slots=True)
-class SelectedScopeIdsCarrier(SelectionModeCarrier):
- selected_scope_ids: tuple[str, ...] = ()
-
-
-@dataclass(frozen=True, slots=True)
-class SelectedScopeIdsArgument(SelectedScopeIdsCarrier):
- """Nominal adapter for optional external selected-scope collections."""
-
- @classmethod
- def from_optional_iterable(
- cls,
- value: Iterable[str] | None,
- ) -> "SelectedScopeIdsArgument":
- if value is None:
- return cls(selected_scope_ids=())
- return cls(selected_scope_ids=tuple(value))
diff --git a/openhcs/desktop/deployment.py b/openhcs/desktop/deployment.py
index 93e42c4f94..154636afce 100644
--- a/openhcs/desktop/deployment.py
+++ b/openhcs/desktop/deployment.py
@@ -24,7 +24,7 @@
from packaging.utils import canonicalize_name
from openhcs import __version__ as OPENHCS_VERSION
-from openhcs.agent.runtime_platform import (
+from formbridge.runtime_platform import (
AgentRuntimePlatform,
MacOSRuntimePlatform,
WindowsRuntimePlatform,
diff --git a/openhcs/desktop/update.py b/openhcs/desktop/update.py
index b9b6bc02bd..5fbcbc5fdd 100644
--- a/openhcs/desktop/update.py
+++ b/openhcs/desktop/update.py
@@ -27,7 +27,7 @@
from python_introspect import dataclass_from_mapping, to_jsonable
from zmqruntime.process_launch import BackgroundProcessLaunchPolicy
-from openhcs.core.selection import AllItems
+from formbridge.selection import AllItems
from openhcs import __version__ as OPENHCS_VERSION
from openhcs.desktop.deployment import (
DESKTOP_RESTART_EXECUTABLE_ENVIRONMENT_VARIABLE,
diff --git a/openhcs/desktop/zmq_version_restart.py b/openhcs/desktop/zmq_version_restart.py
index c21a80fd23..c3b9799934 100644
--- a/openhcs/desktop/zmq_version_restart.py
+++ b/openhcs/desktop/zmq_version_restart.py
@@ -7,9 +7,7 @@
from PyQt6.QtCore import QObject, pyqtSignal
from PyQt6.QtWidgets import QMessageBox
-from pyqt_reactive.services.async_operation_executor import (
- AsyncOperationExecutorClosedError,
-)
+from formbridge.services.async_operation_executor import AsyncOperationExecutorClosedError
from openhcs.core.execution_state import ManagerExecutionState
from openhcs.desktop.restart import DesktopSessionRestart
diff --git a/openhcs/mcp/client_registration.py b/openhcs/mcp/client_registration.py
index f84a9ca6e6..db504cf9fb 100644
--- a/openhcs/mcp/client_registration.py
+++ b/openhcs/mcp/client_registration.py
@@ -24,7 +24,7 @@
from metaclass_registry import AutoRegisterMeta, RegistryFamily, RegistryKeyAttribute
from tomlkit.items import InlineTable
-from openhcs.agent.runtime_platform import (
+from formbridge.runtime_platform import (
AgentRuntimePlatform,
MacOSRuntimePlatform,
WindowsRuntimePlatform,
diff --git a/openhcs/mcp/context.py b/openhcs/mcp/context.py
index c47a6938aa..233399e5cf 100644
--- a/openhcs/mcp/context.py
+++ b/openhcs/mcp/context.py
@@ -37,7 +37,7 @@
from openhcs.agent.services.synthetic_plate_service import (
SyntheticPlateGenerationService,
)
- from openhcs.agent.services.ui_bridge_service import UiBridgeService
+ from formbridge.bridge.service import UiBridgeService
from openhcs.agent.services.viewer_endpoint_discovery import (
ViewerEndpointDiscoveryService,
)
@@ -287,9 +287,9 @@ def runtime_server_service(self) -> "RuntimeServerService":
@property
def ui_bridge_service(self) -> "UiBridgeService":
if self._ui_bridge_service is None:
- from openhcs.agent.services.ui_bridge_service import UiBridgeService
+ from openhcs.agent.services.ui_bridge_service import OpenHCSUiBridgeService
- self._ui_bridge_service = UiBridgeService(path_policy=self.path_policy)
+ self._ui_bridge_service = OpenHCSUiBridgeService(path_policy=self.path_policy)
return self._ui_bridge_service
@property
diff --git a/openhcs/mcp/control_timeout.py b/openhcs/mcp/control_timeout.py
index 429cc126f8..d23f019b46 100644
--- a/openhcs/mcp/control_timeout.py
+++ b/openhcs/mcp/control_timeout.py
@@ -6,7 +6,7 @@
from typing import ClassVar
from openhcs.agent.dto.viewer import VIEWER_WINDOW_CONTROL_TIMEOUT_MS_DEFAULT
-from openhcs.agent.services.ui_bridge_service import DEFAULT_UI_BRIDGE_TIMEOUT_MS
+from formbridge.bridge.service import DEFAULT_UI_BRIDGE_TIMEOUT_MS
MIN_MCP_CONTROL_TIMEOUT_MS = 1
diff --git a/openhcs/mcp/dev_client_commanding.py b/openhcs/mcp/dev_client_commanding.py
index 2b03995a03..d21bdf3af6 100644
--- a/openhcs/mcp/dev_client_commanding.py
+++ b/openhcs/mcp/dev_client_commanding.py
@@ -21,7 +21,7 @@
get_capability_registry,
require_agent_type_contract,
)
-from openhcs.agent.dto.common import AgentCliArgumentSpec, AgentCliRequest
+from formbridge.contracts import AgentCliArgumentSpec, AgentCliRequest
from python_introspect import JsonValue
from openhcs.agent.dto.execution import (
RuntimeServerConnectionToolRequest,
diff --git a/openhcs/mcp/dev_client_commands/ui.py b/openhcs/mcp/dev_client_commands/ui.py
index 230946bfdb..ca175a426d 100644
--- a/openhcs/mcp/dev_client_commands/ui.py
+++ b/openhcs/mcp/dev_client_commands/ui.py
@@ -7,19 +7,19 @@
from typing import cast
-from pyqt_reactive.services.window_snapshot import (
+from formbridge.services.window_snapshot import (
WindowSnapshotCaptureScope,
WindowSnapshotFrameCondition,
)
-from openhcs.core.selection import AllItems, SelectedItems, SelectionScope
+from formbridge.selection import AllItems, SelectedItems, SelectionScope
from openhcs.agent.dto.session import (
DatasetRowState,
SessionEventBatch,
SessionEventsRequest,
)
from openhcs.agent.capabilities import agent_capabilities
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
AllFields,
UiActionInvokeRequest,
UiCodeDocumentApplyRequest,
@@ -30,18 +30,13 @@
UiObjectStateFieldListQuery,
UiObjectStateFieldMutationRequest,
UiObjectStateScopeListRequest,
- UiSelectedPlateWorkflowKind,
UiWidgetActionInvokeRequest,
UiWidgetTreeRequest,
UiWindowSnapshotRequest,
)
-from openhcs.agent.services.ui_bridge_service import (
- DEFAULT_UI_BRIDGE_TIMEOUT_MS,
- UiBridgeGatewayTimeoutError,
-)
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerStateSurfaceIdentityDeclaration,
-)
+from openhcs.agent.dto.ui_bridge import UiSelectedPlateWorkflowKind
+from formbridge.bridge.service import DEFAULT_UI_BRIDGE_TIMEOUT_MS, UiBridgeGatewayTimeoutError
+from openhcs.agent.ui_bridge_identities import DatasetListStateSurfaceIdentity
from openhcs.mcp.dev_client_commanding import (
CapabilityBackedCommandSpec,
McpDevCommandSpec,
@@ -104,7 +99,7 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"surface_id",
nargs="?",
- default=PlateManagerStateSurfaceIdentityDeclaration.require_value(),
+ default=DatasetListStateSurfaceIdentity.require_value(),
)
parser.add_argument(
"--selection-mode",
@@ -166,7 +161,7 @@ class SelectedWorkflowCommandSpec(CapabilityBackedCommandSpec):
capability = agent_capabilities.ui_selected_plate_workflow
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
- state_surface_id = PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ state_surface_id = DatasetListStateSurfaceIdentity.require_value()
parser.add_argument(
"workflow",
choices=tuple(workflow.value for workflow in UiSelectedPlateWorkflowKind),
@@ -247,7 +242,7 @@ async def run_session(
state_surface_tool_arguments(
args,
surface_id=(
- PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ DatasetListStateSurfaceIdentity.require_value()
),
selection_mode=args.poll_selection_mode,
),
diff --git a/openhcs/mcp/dev_client_commands/viewer.py b/openhcs/mcp/dev_client_commands/viewer.py
index 7e8475f5b6..675d75f746 100644
--- a/openhcs/mcp/dev_client_commands/viewer.py
+++ b/openhcs/mcp/dev_client_commands/viewer.py
@@ -4,7 +4,7 @@
import argparse
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from openhcs.agent.capabilities import agent_capabilities
from python_introspect import JsonValue
diff --git a/openhcs/mcp/dev_client_core.py b/openhcs/mcp/dev_client_core.py
index 7c82e2cc69..ad08753d4f 100644
--- a/openhcs/mcp/dev_client_core.py
+++ b/openhcs/mcp/dev_client_core.py
@@ -38,9 +38,9 @@
LocalCapabilitySurfaceProfile,
get_agent_capability,
)
-from openhcs.agent.dto.common import AgentCliRequest, AgentError, AgentResultEnvelope
+from formbridge.contracts import AgentCliRequest, AgentError, AgentResultEnvelope
from openhcs.agent.dto.mcp import McpBoundaryFailure, McpToolErrorResult
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
AllFields,
DefaultDiffFields,
DirtyFields,
@@ -51,25 +51,20 @@
UiBridgeOperationStatus,
UiBridgeOperationWaitRequest,
UiObjectStateFieldFilter,
+ UiStateSurfaceDocument,
+ UiStateSurfaceRequest,
+)
+from openhcs.agent.dto.ui_bridge import (
UiPlateManagerState,
UiSelectedPlateWorkflowKind,
UiSelectedPlateWorkflowRequest,
UiSelectedPlateWorkflowResult,
- UiStateSurfaceDocument,
- UiStateSurfaceRequest,
)
from openhcs.agent.path_policy import AgentPathPolicy
-from openhcs.agent.runtime_platform import AgentRuntimePlatform
-from openhcs.agent.ui_bridge_environment import UiBridgeDescriptorEnvironment
-from openhcs.constants.constants import (
- OrchestratorCompileFailed,
- OrchestratorExecFailed,
- OrchestratorInitFailed,
- OrchestratorState,
-)
+from formbridge.runtime_platform import AgentRuntimePlatform
+from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
from openhcs.core.execution_state import (
ManagerExecutionState,
- TerminalExecutionStatus,
)
from openhcs.core.native_threading import native_thread_count_environment_keys
from openhcs.core.plate_file_inventory import ALL_PLATE_FILE_KINDS
@@ -83,6 +78,8 @@
from openhcs.runtime.import_authority import OpenHCSRuntimeImportAuthority
from openhcs.utils.environment import OpenHCSProcessEnvironment
from openhcs.core.axes import AxisFamily
+from openhcs.authoring.session.operations import SessionOperation
+from openhcs.authoring.session.operations.datasets import DatasetWorkflowOperation
if TYPE_CHECKING:
from mcp.types import ProgressNotification
@@ -341,7 +338,7 @@ def environment(self) -> dict[str, str]:
additional_keys=(
*self.mcp_environment_keys,
*AgentPathPolicy.environment_keys(),
- *UiBridgeDescriptorEnvironment.child_process_environment_keys(),
+ *OpenHCSUiBridgeApplication.descriptor_environment_keys(),
*UIConfigCacheEnvironment.child_process_environment_keys(),
*OpenHCSProcessEnvironment.child_process_environment_keys(),
*native_thread_count_environment_keys(),
@@ -552,91 +549,24 @@ def has_only_agent_error_code(self, code: str) -> bool:
)
-class WorkflowTerminalStateCriterion(ABC, metaclass=AutoRegisterMeta):
- """Registered terminal-state criterion for one selected workflow kind."""
-
- __registry__: ClassVar[
- dict[UiSelectedPlateWorkflowKind, type["WorkflowTerminalStateCriterion"]]
- ] = {}
- __registry_key__ = "workflow"
- __skip_if_no_key__ = True
-
- workflow: ClassVar[UiSelectedPlateWorkflowKind]
- failed_orchestrator_states: ClassVar[tuple[type[OrchestratorState], ...]] = ()
- terminal_state_is_idempotent: ClassVar[bool] = False
-
- @classmethod
- def for_workflow(
- cls,
- workflow: UiSelectedPlateWorkflowKind,
- ) -> "WorkflowTerminalStateCriterion":
- return cls.__registry__[workflow]()
-
- @abstractmethod
- def terminal_for_row(self, row: DatasetRowState) -> bool:
- """Return whether this workflow has reached its terminal row state."""
-
- def failed_for_row(self, row: DatasetRowState) -> bool:
- """Return whether this workflow reached a failed terminal row state."""
- terminal_status = terminal_execution_status(row.terminal_status)
- if terminal_status is not None and terminal_status.counts_as_failed:
- return True
-
- row_orchestrator_state = parse_orchestrator_state(row.orchestrator_state)
- return (
- row_orchestrator_state is not None
- and row_orchestrator_state in self.failed_orchestrator_states
- )
-
-
-class InitWorkflowTerminalStateCriterion(WorkflowTerminalStateCriterion):
- workflow = UiSelectedPlateWorkflowKind.INIT
- failed_orchestrator_states = (OrchestratorInitFailed,)
- terminal_state_is_idempotent = True
-
- def terminal_for_row(self, row: DatasetRowState) -> bool:
- return row.init_pending is False and row.initialized is True
-
-
-class CompileWorkflowTerminalStateCriterion(WorkflowTerminalStateCriterion):
- workflow = UiSelectedPlateWorkflowKind.COMPILE
- failed_orchestrator_states = (OrchestratorCompileFailed,)
-
- def terminal_for_row(self, row: DatasetRowState) -> bool:
- return row.compile_pending is False and row.compiled is True
-
-
-class RunWorkflowTerminalStateCriterion(WorkflowTerminalStateCriterion):
- workflow = UiSelectedPlateWorkflowKind.RUN
- failed_orchestrator_states = (OrchestratorExecFailed,)
-
- def terminal_for_row(self, row: DatasetRowState) -> bool:
- return (
- row.execution_active is False
- and row.queue_position is None
- and row.terminal_status is not None
- )
-
-
@dataclass(frozen=True, slots=True)
class WorkflowStatePollPolicy:
- """Terminal-state policy for one selected PlateManager workflow."""
+ """Terminal-state policy for one selected dataset workflow step."""
- criterion: WorkflowTerminalStateCriterion
+ operation: type[DatasetWorkflowOperation]
@classmethod
def from_workflow_text(cls, workflow: str) -> "WorkflowStatePollPolicy":
- return cls(
- criterion=WorkflowTerminalStateCriterion.for_workflow(
- UiSelectedPlateWorkflowKind(workflow)
- )
- )
+ operation = SessionOperation.named(UiSelectedPlateWorkflowKind(workflow).value)
+ if not issubclass(operation, DatasetWorkflowOperation):
+ raise TypeError(f"{workflow!r} is not a dataset workflow step.")
+ return cls(operation=operation)
def terminal_for_row(self, row: DatasetRowState) -> bool:
- return self.criterion.terminal_for_row(row)
+ return self.operation.terminal_for_row(row)
def failed_for_row(self, row: DatasetRowState) -> bool:
- return self.criterion.failed_for_row(row)
+ return self.operation.failed_for_row(row)
def can_evaluate(
self,
@@ -648,7 +578,7 @@ def can_evaluate(
return (
baseline is None
or baseline.changed_by(result)
- or self.criterion.terminal_state_is_idempotent
+ or self.operation.terminal_state_is_idempotent
)
@@ -2474,25 +2404,6 @@ def workflow_poll_target_rows(
return tuple(row for row in state.rows if row.scope_id in target_scope_ids)
-def terminal_execution_status(
- value: str | None,
-) -> type[TerminalExecutionStatus] | None:
- if value is None:
- return None
- try:
- return TerminalExecutionStatus.named(value)
- except ValueError:
- return None
-
-
-def parse_orchestrator_state(
- value: str | None,
-) -> type[OrchestratorState] | None:
- if value is None:
- return None
- return OrchestratorState.named(value)
-
-
def workflow_poll_summary_result(
*,
workflow: str | None = None,
diff --git a/openhcs/mcp/dev_client_renderers/object_state.py b/openhcs/mcp/dev_client_renderers/object_state.py
index 5d986dd53e..c88c746312 100644
--- a/openhcs/mcp/dev_client_renderers/object_state.py
+++ b/openhcs/mcp/dev_client_renderers/object_state.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
AllFields,
UiCatalogPageMetadata,
UiObjectStateFieldHelpResult,
diff --git a/openhcs/mcp/dev_client_renderers/pipeline.py b/openhcs/mcp/dev_client_renderers/pipeline.py
index cb3f07ba00..9d3c449c2d 100644
--- a/openhcs/mcp/dev_client_renderers/pipeline.py
+++ b/openhcs/mcp/dev_client_renderers/pipeline.py
@@ -5,7 +5,8 @@
from inspect import getdoc
from openhcs.agent.capabilities import agent_capabilities
-from openhcs.agent.dto.common import AgentError, RenderedSource
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import RenderedSource
from openhcs.agent.dto.execution import (
ArtifactInputPlanSummary,
ArtifactMaterializationPlanSummary,
diff --git a/openhcs/mcp/dev_client_renderers/ui_bridge.py b/openhcs/mcp/dev_client_renderers/ui_bridge.py
index df90ed377a..3fb125c6be 100644
--- a/openhcs/mcp/dev_client_renderers/ui_bridge.py
+++ b/openhcs/mcp/dev_client_renderers/ui_bridge.py
@@ -9,9 +9,9 @@
from openhcs.agent.dto.session import DatasetRowState, PipelineStepState
from openhcs.agent.capabilities import agent_capabilities
-from openhcs.agent.dto.common import AgentWarning
+from formbridge.contracts import AgentWarning
from openhcs.agent.dto.mcp import McpServerHealthResult
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
QT_TOP_LEVEL_WINDOW_KIND,
UiActionCatalog,
UiActionInvokeResult,
@@ -23,14 +23,8 @@
UiCodeDocumentCatalog,
UiCodeDocumentSummary,
UiCodeDocumentValidationResult,
- UiDebugActionState,
- UiDebugRuntimeFrameState,
UiLiveOverviewSection,
UiLiveOverviewState,
- UiPipelineDebugSessionState,
- UiPipelineEditorState,
- UiPlateManagerState,
- UiSelectedPlateWorkflowResult,
UiSnapshotRef,
UiStateSurfaceCatalog,
UiStateSurfaceDocument,
@@ -43,11 +37,21 @@
UiWindowCatalog,
UiWindowSummary,
)
+from openhcs.agent.dto.ui_bridge import (
+ UiDebugActionState,
+ UiDebugRuntimeFrameState,
+ UiPipelineDebugSessionState,
+ UiPipelineEditorState,
+ UiPlateManagerState,
+ UiSelectedPlateWorkflowResult,
+)
from openhcs.agent.ui_bridge_identities import (
MainWindowWidgetIdentity,
PipelineDebugSessionStateSurfaceIdentityDeclaration,
PipelineEditorStateSurfaceIdentityDeclaration,
- PlateManagerStateSurfaceIdentityDeclaration,
+ DatasetListStateSurfaceIdentity,
+)
+from formbridge.bridge.identities import (
UiLiveOverviewStateSurfaceIdentityDeclaration,
UiStateSurfaceIdentityDeclarationBase,
)
@@ -316,7 +320,7 @@ def _section_lines(cls, section: UiLiveOverviewSection) -> list[str]:
class PlateManagerStateSurfaceRenderer(UiStateSurfaceStateRenderer):
"""Compact renderer for PlateManager state-surface rows."""
- surface_identity = PlateManagerStateSurfaceIdentityDeclaration
+ surface_identity = DatasetListStateSurfaceIdentity
output_contract = UiPlateManagerState
@classmethod
diff --git a/openhcs/mcp/dev_client_renderers/viewer.py b/openhcs/mcp/dev_client_renderers/viewer.py
index 54255f24f0..bc55326a86 100644
--- a/openhcs/mcp/dev_client_renderers/viewer.py
+++ b/openhcs/mcp/dev_client_renderers/viewer.py
@@ -16,7 +16,7 @@
RuntimeServerInfo,
RuntimeServerScanResult,
)
-from openhcs.agent.dto.ui_bridge import UiWindowSnapshotResult
+from formbridge.bridge.dto import UiWindowSnapshotResult
from openhcs.agent.dto.viewer import (
ViewerWindowImageSampleResult,
ViewerWindowLayerIsolationResult,
diff --git a/openhcs/mcp/dev_client_rendering.py b/openhcs/mcp/dev_client_rendering.py
index d842d9947d..90edee56e8 100644
--- a/openhcs/mcp/dev_client_rendering.py
+++ b/openhcs/mcp/dev_client_rendering.py
@@ -25,7 +25,7 @@
get_agent_capability,
)
from openhcs.agent.dto.authoring import AuthoringContextRequest
-from openhcs.agent.dto.common import AgentError, AgentWarning
+from formbridge.contracts import AgentError, AgentWarning
DEFAULT_CODE_DOCUMENT_MAX_CHARS = 2_000
PresentationValue = TypeVar("PresentationValue")
diff --git a/openhcs/mcp/execution.py b/openhcs/mcp/execution.py
index 39b179072a..6a2758e019 100644
--- a/openhcs/mcp/execution.py
+++ b/openhcs/mcp/execution.py
@@ -4,19 +4,16 @@
import asyncio
from collections.abc import Callable, Coroutine
-from concurrent.futures import Future
from contextvars import copy_context
import threading
from typing import Any, ParamSpec, TypeVar
-from PyQt6.QtCore import QCoreApplication, QEventLoop, QThread
-from polystore import cleanup_backend_connections
-from pyqt_reactive.core.future_completion import FutureCompletion
-from pyqt_reactive.services.async_operation_executor import AsyncOperationExecutor
-from pyqt_reactive.services.ui_thread_dispatch import (
- UiThreadDispatcher,
- UiThreadDispatchTimeoutError,
+from formbridge.services.async_operation_executor import AsyncOperationExecutor
+from formbridge.services.main_thread_dispatch import (
+ MainThreadDispatchTimeoutError,
+ QueuedMainThreadDispatcher,
)
+from polystore import cleanup_backend_connections
from openhcs.agent.exceptions import AgentFacingErrorMixin
@@ -24,31 +21,29 @@
ResultT = TypeVar("ResultT")
-class McpQueuedCallNotStartedError(AgentFacingErrorMixin, UiThreadDispatchTimeoutError):
- """Original dispatch owner cancelled this callback before it began."""
+class McpQueuedCallNotStartedError(AgentFacingErrorMixin, MainThreadDispatchTimeoutError):
+ """The dispatcher cancelled this callback before it began."""
agent_error_code = "mcp_call_not_started"
agent_error_hint = (
- "The Qt dispatcher cancelled this queued callback before invocation. "
+ "The main-thread dispatcher cancelled this queued callback before invocation. "
"This call did not execute; it created no runtime process or handle. "
"This is not an uncertain started operation. Preserve its failed receipt "
"and resolve the occupied main-thread work before a separate new attempt."
)
-class McpMainThreadDispatcher(UiThreadDispatcher):
- """Extend the original Qt dispatch owner with request-context propagation.
+class McpMainThreadDispatcher(QueuedMainThreadDispatcher):
+ """Run MCP application work on the process main thread with request context.
- ObjectState operations also require the process main thread in a headless
- server. Absence of a Qt application must not authorize a worker thread.
+ ObjectState operations require the process main thread in a headless server,
+ so the dispatcher must be built there.
"""
- @staticmethod
- def _is_ui_thread() -> bool:
- return (
- threading.current_thread() is threading.main_thread()
- and UiThreadDispatcher._is_ui_thread()
- )
+ def __init__(self) -> None:
+ if threading.current_thread() is not threading.main_thread():
+ raise RuntimeError("The MCP dispatcher must be built on the process main thread.")
+ super().__init__()
def call(
self, callback: Callable[[], ResultT], *, timeout_ms: int = 5000
@@ -59,35 +54,28 @@ def call(
)
@staticmethod
- def _dispatch_timeout_error() -> UiThreadDispatchTimeoutError:
- """Project only the original dispatch owner's successful cancellation.
+ def _dispatch_timeout_error() -> MainThreadDispatchTimeoutError:
+ """Project only the dispatcher's successful cancellation.
- The owner passes this error to its queued call's atomic cancel(). A
+ The dispatcher passes this error to its queued call's atomic cancel(). A
callback's own exception, including a dispatch timeout after a side
effect, remains the callback outcome and never passes through this hook.
"""
- return McpQueuedCallNotStartedError("Timed out waiting for UI thread dispatch.")
+ return McpQueuedCallNotStartedError("Timed out waiting for main-thread dispatch.")
async def invoke(self, callback: Callable[[], ResultT]) -> ResultT:
- """Await the original affine call without blocking SDK notifications."""
- if self._is_ui_thread():
+ """Await one main-thread call without blocking SDK notifications."""
+ if self._is_owner_thread():
return self.call(callback)
return await asyncio.to_thread(self.call, callback)
class McpTransportExecutor(AsyncOperationExecutor):
- """Run the SDK loop off-main while the original Qt dispatcher owns calls.
-
- QCoreApplication supplies only an event dispatcher, not a GUI or native
- execution server. Compiler, catalog and kernel placement remain unchanged.
- """
+ """Run the SDK loop off-main while the main thread serves dispatched calls."""
def __init__(self) -> None:
if threading.current_thread() is not threading.main_thread():
raise RuntimeError("MCP transport must be started on the process main thread.")
- self._application = QCoreApplication.instance() or QCoreApplication([])
- if QThread.currentThread() != self._application.thread():
- raise RuntimeError("MCP transport requires the Qt application main thread.")
super().__init__(max_workers=1)
self.dispatcher = McpMainThreadDispatcher()
@@ -97,22 +85,13 @@ def run(
*args: ParametersT.args,
**kwargs: ParametersT.kwargs,
) -> ResultT:
- """Join one transport using event-driven completion, not a new poller."""
- loop = QEventLoop()
+ """Serve main-thread calls until the transport operation finishes."""
future = self.submit(operation, *args, **kwargs)
-
- def finished(completed: Future[ResultT]) -> None:
- loop.quit()
-
- completion = FutureCompletion(future, finished)
- if not future.done():
- loop.exec()
- # Keep the original completion relay alive through the event loop.
- del completion
+ self.dispatcher.run_until(future)
return future.result()
def close(self) -> None:
- """Release process resources on their original main-thread owner.
+ """Release process resources on the main thread.
Both transports reach this boundary after their SDK operation finishes.
PolyStore owns the lazy ImageJ context and registered resource cleanup;
diff --git a/openhcs/mcp/server.py b/openhcs/mcp/server.py
index 1ca97a4b91..e03f3cd404 100644
--- a/openhcs/mcp/server.py
+++ b/openhcs/mcp/server.py
@@ -36,11 +36,11 @@
from pydantic import WithJsonSchema
from pydantic.json_schema import GenerateJsonSchema
from metaclass_registry import AutoRegisterMeta
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
from python_introspect import JsonValue, dataclass_from_mapping, to_jsonable
from zmqruntime.config import TransportMode
from zmqruntime.startup import EndpointStartupStatus
+import formbridge as formbridge_package
import openhcs as openhcs_package
from openhcs.agent.capabilities import (
AgentCapabilityDeclaration,
@@ -54,12 +54,12 @@
get_capability_registry,
require_agent_type_contract,
)
-from openhcs.agent.dto.common import (
+from formbridge.contracts import (
AGENT_PARAMETER_DESCRIPTION_METADATA_KEY,
AGENT_PARAMETER_PRODUCER_OUTPUT_CONTRACT_METADATA_KEY,
- SCHEMA_VERSION,
AgentError,
)
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.execution import (
ExecutionConnectionSpec,
)
@@ -68,10 +68,7 @@
McpServerStaleErrorResult,
McpToolErrorResult,
)
-from openhcs.agent.dto.ui_bridge import (
- UiBridgeConnectionRequest,
- UiBridgeConnectionSpec,
-)
+from formbridge.bridge.dto import UiBridgeConnectionRequest, UiBridgeConnectionSpec
from openhcs.agent.dto.viewer import ViewerWindowControlRequest
from openhcs.agent.exceptions import AgentFacingErrorMixin
from openhcs.agent.knowledge_manifest import knowledge_base_source_paths_from_manifest
@@ -243,8 +240,8 @@ def _package_python_source_paths(package) -> tuple[Path, ...]:
Path(create_agent_context.__code__.co_filename).resolve(),
Path(get_capability_registry.__code__.co_filename).resolve(),
Path(to_jsonable.__code__.co_filename).resolve(),
- _source_path_for_type(WindowSnapshotCaptureScope),
*_package_python_source_paths(openhcs_package),
+ *_package_python_source_paths(formbridge_package),
*MCP_SERVER_PACKAGED_RESOURCE_PATHS,
)
)
diff --git a/openhcs/processing/custom_functions/events.py b/openhcs/processing/custom_functions/events.py
index 151259b07e..f564aa2bc3 100644
--- a/openhcs/processing/custom_functions/events.py
+++ b/openhcs/processing/custom_functions/events.py
@@ -1,6 +1,6 @@
"""Domain change events for process-local custom-function declarations."""
-from pyqt_reactive.weak_events import WeakCallbackEvent
+from formbridge.weak_events import WeakCallbackEvent
class CustomFunctionChangedEvent(WeakCallbackEvent[[]]):
diff --git a/openhcs/pyqt_gui/config.py b/openhcs/pyqt_gui/config.py
index 2f69a9b820..dd2d42aee8 100644
--- a/openhcs/pyqt_gui/config.py
+++ b/openhcs/pyqt_gui/config.py
@@ -7,21 +7,16 @@
"""
import logging
-import os
-import secrets
-import uuid
-from collections.abc import Mapping
from dataclasses import dataclass, field, replace
from enum import Enum
from pathlib import Path
from typing import Annotated
from pyqt_reactive.qt_types import QtKeySequenceText
-from pyqt_reactive.services.system_monitor_config import PerformanceMonitorConfig
-from pyqt_reactive.strategies.preview_formatting import FormattingConfig
+from formbridge.services.system_monitor_config import PerformanceMonitorConfig
+from formbridge.strategies.preview_formatting import FormattingConfig
from python_introspect import (
EnvironmentVariable,
- overlay_dataclass_from_environment,
validate_annotated_dataclass,
)
from zmqruntime.config import (
@@ -33,12 +28,8 @@
)
from zmqruntime.transport import get_default_transport_mode
-from openhcs.agent.dto.execution import ExecutionConnectionSpec
-from openhcs.agent.runtime_platform import AgentRuntimePlatform
-from openhcs.agent.services.ui_bridge_service import (
- UiBridgeDescriptorDirectoryAuthority,
-)
-from openhcs.agent.ui_bridge_environment import UiBridgeDescriptorEnvironment
+from formbridge.bridge.server import UiBridgeServerConfig
+from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
from openhcs.core.config import GlobalPipelineConfig
from openhcs.core.config_cache import ConfigCacheSpec
from openhcs.core.xdg_paths import get_openhcs_log_dir
@@ -200,7 +191,7 @@ def __post_init__(self) -> None:
@dataclass(frozen=True, slots=True)
-class AgentUiBridgeConfig(ExecutionConnectionSpec):
+class AgentUiBridgeConfig(UiBridgeServerConfig):
"""Publish an authenticated local endpoint for inspecting the live PyQt UI.
The bridge exposes ObjectState, window, widget, action, log, and viewer
@@ -236,8 +227,6 @@ class AgentUiBridgeConfig(ExecutionConnectionSpec):
default selects the platform preference; an explicit ``None`` uses the
bridge's TCP fallback.
"""
- persistent: bool = True
- """Advertise bridge connections as reusable across successive client requests."""
enabled: Annotated[
bool,
EnvironmentVariable("OPENHCS_ENABLE_UI_BRIDGE"),
@@ -250,7 +239,7 @@ class AgentUiBridgeConfig(ExecutionConnectionSpec):
descriptor_directory_path: Annotated[
Path | None,
EnvironmentVariable(
- UiBridgeDescriptorEnvironment.descriptor_directory_path_key,
+ OpenHCSUiBridgeApplication.descriptor_directory_variable,
clear_on_empty=True,
),
] = None
@@ -262,7 +251,7 @@ class AgentUiBridgeConfig(ExecutionConnectionSpec):
descriptor_file_path: Annotated[
Path | None,
EnvironmentVariable(
- UiBridgeDescriptorEnvironment.descriptor_file_path_key,
+ OpenHCSUiBridgeApplication.descriptor_file_variable,
clear_on_empty=True,
),
] = None
@@ -271,59 +260,6 @@ class AgentUiBridgeConfig(ExecutionConnectionSpec):
When set, this overrides ``descriptor_directory_path``. The server removes
the descriptor during orderly shutdown.
"""
- bridge_instance_id: NonBlankString | None = field(
- default=None,
- metadata={"ui_hidden": True},
- )
- """Stable bridge instance identifier. None generates a fresh ui-prefixed identifier at startup."""
- auth_token: NonBlankString | None = field(
- default=None,
- metadata={"ui_hidden": True},
- )
- """Authentication token required by bridge clients. None generates a random token at startup."""
- poll_timeout_ms: PositiveInteger = 100
- """Maximum milliseconds the bridge server waits for socket activity per poll.
-
- Lower values reduce shutdown latency while increasing idle wake-ups.
- """
- shutdown_timeout_seconds: PositiveFloat = 2.0
- """Maximum seconds the GUI waits for the bridge thread during shutdown."""
-
- @classmethod
- def from_environment(
- cls,
- base: "AgentUiBridgeConfig | None" = None,
- ) -> "AgentUiBridgeConfig":
- if base is None:
- base = cls()
- return overlay_dataclass_from_environment(base)
-
- def resolve_bridge_instance_id(self) -> str:
- if self.bridge_instance_id is not None:
- return self.bridge_instance_id
- return f"ui-{uuid.uuid4()}"
-
- def resolve_auth_token(self) -> str:
- if self.auth_token is not None:
- return self.auth_token
- return secrets.token_urlsafe(32)
-
- def descriptor_path_for(self, bridge_instance_id: str) -> Path:
- if self.descriptor_file_path is not None:
- return AgentRuntimePlatform.resolved_path(
- self.descriptor_file_path
- )
- return (
- self.descriptor_directory_or_default()
- / f"ui_bridge_{bridge_instance_id}.json"
- ).resolve(strict=False)
-
- def descriptor_directory_or_default(self) -> Path:
- if self.descriptor_directory_path is not None:
- return AgentRuntimePlatform.resolved_path(
- self.descriptor_directory_path
- )
- return UiBridgeDescriptorDirectoryAuthority.default_descriptor_dir()
@dataclass(frozen=True)
diff --git a/openhcs/pyqt_gui/main.py b/openhcs/pyqt_gui/main.py
index fc933bcc43..faaac1c680 100644
--- a/openhcs/pyqt_gui/main.py
+++ b/openhcs/pyqt_gui/main.py
@@ -29,12 +29,11 @@
QWidget,
)
from pyqt_reactive.animation import WindowFlashOverlay
+from pyqt_reactive.bridge.host import UiBridgeHostWindow
from pyqt_reactive.services.application_shortcuts import ShortcutLifecycle
from pyqt_reactive.services.time_travel_navigation import TimeTravelNavigator
from pyqt_reactive.services.window_manager import WindowManager
-from pyqt_reactive.services.zmq_server_scan_service import (
- EndpointObservationSnapshot,
-)
+from formbridge.services.zmq_server_scan_service import EndpointObservationSnapshot
from pyqt_reactive.widgets import (
StatusIndicator,
StatusState,
@@ -44,18 +43,18 @@
from pyqt_reactive.widgets.system_monitor import SystemMonitorWidget
from zmqruntime import EndpointConnectionCancelledError
-from openhcs.core.selection import AllItems, SelectedItems
+from formbridge.selection import AllItems, SelectedItems
from openhcs.agent.services.endpoint_function_catalog_service import (
ZMQFunctionCatalogService,
)
from openhcs.agent.ui_bridge_identities import (
MainWindowWidgetIdentity,
PipelineEditorWidgetIdentity,
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
SystemMonitorWindowIdentity,
- UiLiveOverviewStateSurfaceIdentityDeclaration,
ZmqServerManagerWindowIdentity,
)
+from formbridge.bridge.identities import UiLiveOverviewStateSurfaceIdentityDeclaration
from openhcs.authoring.session.events import (
ExecutionStateChanged,
ProgressAdvanced,
@@ -68,7 +67,7 @@
)
from openhcs.authoring.session.session import DispatcherThread, Session
from openhcs.core.config import GlobalPipelineConfig
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from openhcs.authoring.session.operations.application import (
CheckForUpdates,
ExitApplication,
@@ -103,9 +102,7 @@
build_main_window_specs,
)
from openhcs.pyqt_gui.services.service_adapter import PyQtServiceAdapter
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- UiOwnedStateSurfaceDeclaration,
-)
+from formbridge.bridge.contracts import UiOwnedStateSurfaceDeclaration
from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
from openhcs.pyqt_gui.windows.dual_editor_window import (
select_step_editor_tab_after_time_travel,
@@ -175,7 +172,7 @@ def create_pipeline_editor_widget(self, session: Session):
)
-class OpenHCSMainWindow(QMainWindow):
+class OpenHCSMainWindow(QMainWindow, UiBridgeHostWindow):
"""
Main OpenHCS PyQt6 application window.
@@ -413,7 +410,7 @@ def _create_ui_bridge_server(self, bridge_config, transport_config):
from openhcs.pyqt_gui.services.ui_bridge_composition import (
OpenHCSUiBridgeCompositionRoot,
)
- from openhcs.pyqt_gui.services.ui_bridge_server import UiBridgeControlServer
+ from formbridge.bridge.server import UiBridgeControlServer
return UiBridgeControlServer(
OpenHCSUiBridgeCompositionRoot.for_main_window(self).build_service(),
@@ -498,8 +495,8 @@ def setup_ui(self):
)
plate_manager_pane = DockPane.create(
main_window=self,
- window_id=PlateManagerWidgetIdentity.require_value(),
- title=PlateManagerWidgetIdentity.require_title(),
+ window_id=DatasetListWidgetIdentity.require_value(),
+ title=DatasetListWidgetIdentity.require_title(),
widget=self.plate_manager_widget,
manager_header=self.plate_manager_widget.manager_header,
)
@@ -715,7 +712,7 @@ def setup_menu_bar(self):
system_monitor_action.triggered.connect(self.show_system_monitor)
view_menu.addAction(system_monitor_action)
- plate_action = QAction(f"&{PlateManagerWidgetIdentity.require_title()}", self)
+ plate_action = QAction(f"&{DatasetListWidgetIdentity.require_title()}", self)
plate_action.triggered.connect(self.show_plate_manager)
view_menu.addAction(plate_action)
self.shortcut_lifecycle.bind_menu_action(
diff --git a/openhcs/pyqt_gui/services/code_document_normalization.py b/openhcs/pyqt_gui/services/code_document_normalization.py
index e54526e317..9764ef99ce 100644
--- a/openhcs/pyqt_gui/services/code_document_normalization.py
+++ b/openhcs/pyqt_gui/services/code_document_normalization.py
@@ -7,7 +7,7 @@
from typing import TypeVar
from metaclass_registry import AutoRegisterMeta
-from pyqt_reactive.services.function_pattern_code_document import FunctionPatternValue
+from formbridge.services.function_pattern_code_document import FunctionPatternValue
from python_introspect import AnnotatedDataclassValidationMixin
from openhcs.core.config_document import ConfigDocumentAuthority
@@ -141,7 +141,7 @@ def normalize_source(
clean_mode: bool,
) -> str:
del declaration_type
- from pyqt_reactive.services.function_pattern_code_document import (
+ from formbridge.services.function_pattern_code_document import (
FunctionPatternCodeDocumentService,
)
diff --git a/openhcs/pyqt_gui/services/embedded_code_documents.py b/openhcs/pyqt_gui/services/embedded_code_documents.py
index 6126b9447c..0065c6f5e3 100644
--- a/openhcs/pyqt_gui/services/embedded_code_documents.py
+++ b/openhcs/pyqt_gui/services/embedded_code_documents.py
@@ -6,7 +6,7 @@
from typing import ClassVar
from metaclass_registry import AutoRegisterMeta
-from pyqt_reactive.services.window_code_document import WindowCodeDocumentDriver
+from formbridge.services.window_code_document import WindowCodeDocumentDriver
from pyqt_reactive.services.window_manager import WindowManager
from PyQt6.QtWidgets import QWidget
diff --git a/openhcs/pyqt_gui/services/function_step_code_document.py b/openhcs/pyqt_gui/services/function_step_code_document.py
index 42c6197a1a..cf8a5b7cba 100644
--- a/openhcs/pyqt_gui/services/function_step_code_document.py
+++ b/openhcs/pyqt_gui/services/function_step_code_document.py
@@ -4,7 +4,7 @@
from collections.abc import Callable
-from pyqt_reactive.services.window_code_document import (
+from formbridge.services.window_code_document import (
PYTHON_MIME_TYPE,
WindowCodeDocument,
WindowCodeDocumentDriver,
diff --git a/openhcs/pyqt_gui/services/main_window_workflows.py b/openhcs/pyqt_gui/services/main_window_workflows.py
index 2f2e3afcd0..9b15ecc8eb 100644
--- a/openhcs/pyqt_gui/services/main_window_workflows.py
+++ b/openhcs/pyqt_gui/services/main_window_workflows.py
@@ -24,10 +24,10 @@
KnowledgeBaseWindowIdentity,
LogViewerWindowIdentity,
PipelineEditorWidgetIdentity,
- PlateManagerWidgetIdentity,
- UiStableWindowIdentityDeclaration,
+ DatasetListWidgetIdentity,
ZmqServerManagerWindowIdentity,
)
+from formbridge.bridge.identities import UiStableWindowIdentityDeclaration
from openhcs.agent.dto.session import PipelineFileRequest
from openhcs.authoring.session.operations.pipelines import LoadPipelineFile
from openhcs.core.config import GlobalPipelineConfig
@@ -42,7 +42,7 @@
if TYPE_CHECKING:
from openhcs.pyqt_gui.config import AgentUiBridgeConfig
from openhcs.pyqt_gui.services.service_adapter import PyQtServiceAdapter
- from openhcs.pyqt_gui.services.ui_bridge_server import UiBridgeControlServer
+ from formbridge.bridge.server import UiBridgeControlServer
from openhcs.pyqt_gui.widgets.pipeline_editor import PipelineEditorWidget
from openhcs.runtime.zmq_config import OpenHCSZMQConfig
@@ -85,7 +85,7 @@ def build_main_window_specs() -> dict[str, WindowSpec]:
definitions = (
MainWindowSpecDefinition(
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
PlateManagerWindow,
True,
),
diff --git a/openhcs/pyqt_gui/services/reactor_providers.py b/openhcs/pyqt_gui/services/reactor_providers.py
index 76887b99b9..42bfc8aaef 100644
--- a/openhcs/pyqt_gui/services/reactor_providers.py
+++ b/openhcs/pyqt_gui/services/reactor_providers.py
@@ -15,22 +15,22 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypeVar
-from pyqt_reactive.core.log_utils import LogFileInfo
-from pyqt_reactive.protocols import (
- CodegenProviderABC,
+from formbridge.core.log_utils import LogFileInfo
+from formbridge.protocols.codegen_provider import CodegenProviderABC, register_codegen_provider
+from formbridge.protocols.component_selection import (
ComponentSelectionProviderABC,
- FormGenConfig,
FunctionSelectionProviderABC,
- LogDiscoveryProviderABC,
- ServerScanProviderABC,
- register_codegen_provider,
register_component_selection_provider,
register_function_selection_provider,
+)
+from formbridge.protocols.form_config import FormGenConfig, set_form_config
+from formbridge.protocols.log_providers import (
+ LogDiscoveryProviderABC,
+ ServerScanProviderABC,
register_log_discovery_provider,
- register_preview_formatter,
register_server_scan_provider,
- set_form_config,
)
+from formbridge.protocols.preview_formatter import register_preview_formatter
import openhcs.serialization.pycodify_formatters # noqa: F401
from openhcs.agent.services.endpoint_function_catalog_service import (
@@ -138,9 +138,7 @@ def __init__(
def scan_for_server_logs(self) -> list[LogFileInfo]:
from pathlib import Path
- from pyqt_reactive.services.zmq_server_scan_service import (
- ZMQServerScanService,
- )
+ from formbridge.services.zmq_server_scan_service import ZMQServerScanService
from zmqruntime.execution.logs import ExecutionWorkerLogObservation
from openhcs.core.streaming_config_factory import get_all_streaming_ports
@@ -212,7 +210,7 @@ def _grouping_axis(group_by: Any):
return axis
def _get_plate_manager(self):
- from pyqt_reactive.services.service_registry import ServiceRegistry
+ from formbridge.services.service_registry import ServiceRegistry
from openhcs.pyqt_gui.widgets.plate_manager import PlateManagerWidget
diff --git a/openhcs/pyqt_gui/services/service_adapter.py b/openhcs/pyqt_gui/services/service_adapter.py
index cde5929286..8b67096b9d 100644
--- a/openhcs/pyqt_gui/services/service_adapter.py
+++ b/openhcs/pyqt_gui/services/service_adapter.py
@@ -5,8 +5,9 @@
from typing import Optional
from PyQt6.QtWidgets import QApplication, QFileDialog, QMessageBox, QWidget
-from pyqt_reactive.core.path_cache import PathCacheKey, cache_path, get_initial_path
-from pyqt_reactive.services.async_operation_executor import AsyncOperationExecutor
+from formbridge.core.path_cache import PathCacheKey, cache_path, get_initial_path
+from formbridge.services.async_operation_executor import AsyncOperationExecutor
+from formbridge.services.host_services import UiHostServices
from pyqt_reactive.services.ui_thread_dispatch import UiThreadDispatcher
from pyqt_reactive.theming import ColorScheme, ThemeManager
@@ -14,7 +15,7 @@
logger = logging.getLogger(__name__)
-class PyQtServiceAdapter:
+class PyQtServiceAdapter(UiHostServices):
"""Qt dialogs, async execution, theme and config access for GUI widgets."""
def __init__(self, main_window: QWidget):
diff --git a/openhcs/pyqt_gui/services/ui_agent_bridge.py b/openhcs/pyqt_gui/services/ui_agent_bridge.py
index f24b58248b..7b04cfdaf9 100644
--- a/openhcs/pyqt_gui/services/ui_agent_bridge.py
+++ b/openhcs/pyqt_gui/services/ui_agent_bridge.py
@@ -1,167 +1,45 @@
-"""In-process PyQt implementation of the OpenHCS UI agent bridge."""
+"""The OpenHCS GUI's in-process UI bridge and its plate-manager code document."""
from __future__ import annotations
import ast
-import datetime as _datetime
-import hashlib
-import threading
-import time
-import uuid
-from collections.abc import Callable
from dataclasses import dataclass, replace
-from functools import partial
from pathlib import Path
-from typing import TYPE_CHECKING, TypeVar
+from typing import TYPE_CHECKING
-from objectstate.object_state import ObjectStateRegistry
-from pyqt_reactive.services.ui_thread_dispatch import (
- UiThreadDispatcher,
-)
+from formbridge.bridge.dto import UiActionInvokeRequest
+from formbridge.bridge.in_process import UiAgentBridgeService
+from formbridge.bridge.service import serves
+from formbridge.contracts import AgentError
+from formbridge.services.main_thread_dispatch import MainThreadDispatcher
+from pyqt_reactive.services.ui_thread_dispatch import UiThreadDispatcher
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
- AgentError,
- AgentWarning,
- AgentResultEnvelope,
-)
-from python_introspect import project_dataclass, to_jsonable
-from openhcs.agent.services.function_catalog_service import (
- FunctionCatalogServiceABC,
- UnknownFunctionIdError,
-)
-from openhcs.core.function_reference import (
- FunctionReference,
- RegistryFunctionReference,
-)
-from openhcs.agent.dto.ui_bridge import (
- ActionRejected,
- OperationCompleted,
- OperationFailed,
- OperationNotFound,
- OperationRunning,
- OperationUnavailable,
- UNKNOWN_UI_BRIDGE_OPERATION_ROUTE,
- UiActionCatalog,
- UiActionIdentity,
- UiActionInvokeRequest,
- UiActionInvokeResult,
- UiBranchCatalog,
- UiBranchRef,
- UiBranchSwitchRequest,
- UiBridgeConnectionSpec,
- UiBridgeOperationIdentity,
- UiBridgeOperationRef,
- UiBridgeOperationRoute,
- UiBridgeOperationStatus,
- UiBridgeOperationStatusRequest,
- UiBridgeStatus,
- UiCodeDocument,
- UiCodeDocumentApplyRequest,
- UiCodeDocumentApplyResult,
- UiCodeDocumentCatalog,
- UiCodeDocumentIdentity,
- UiCodeDocumentRequest,
- UiCodeDocumentValidationRequest,
- UiCodeDocumentValidationResult,
- UiLiveOverviewItem,
- UiLiveOverviewMetric,
- UiLiveOverviewSection,
- UiLiveOverviewSeverity,
- UiMutationReceipt,
- UiMutationRequestToken,
- UiObjectStateFieldMutationRequest,
- UiObjectStateFieldMutationResult,
- UiObjectStateScopeCatalog,
- UiObjectStateScopeListRequest,
- UiObjectStateScopeVisibility,
- UiSelectedPlateWorkflowRequest,
- UiSelectedPlateWorkflowResult,
- UiSnapshotCatalog,
- UiSnapshotListRequest,
- UiSnapshotRef,
- UiSnapshotRestoreRequest,
- UiSnapshotRestoreResult,
- UiStateSurfaceCatalog,
- UiStateSurfaceDocument,
- UiStateSurfaceRequest,
- UiTimeTravelHeadRequest,
- UiWidgetActionInvokeRequest,
- UiWidgetActionInvokeResult,
- UiWidgetTreeRequest,
- UiWidgetTreeResult,
- UiWindowCatalog,
- UiWindowCloseRequest,
- UiWindowCloseResult,
- UiWindowFocusRequest,
- UiWindowFocusResult,
- UiWindowNavigateRequest,
- UiWindowNavigateResult,
- UiWindowSnapshotRequest,
- UiWindowSnapshotResult,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.session import (
SessionEventBatch,
SessionEventSummary,
SessionEventsRequest,
)
+from openhcs.agent.dto.ui_bridge import (
+ UiSelectedPlateWorkflowRequest,
+ UiSelectedPlateWorkflowResult,
+)
from openhcs.agent.services.ui_bridge_service import (
- UiBridgeApplyDocumentOperation,
- UiBridgeCloseWindowOperation,
- UiBridgeFocusWindowOperation,
- UiBridgeGatewayABC,
- UiBridgeGetDocumentOperation,
- UiBridgeGetOperationStatusOperation,
- UiBridgeGetStateSurfaceOperation,
- UiBridgeInvokeActionOperation,
- UiBridgeInvokeWidgetActionOperation,
- UiBridgeListActionsOperation,
- UiBridgeListBranchesOperation,
- UiBridgeListDocumentsOperation,
- UiBridgeListObjectStateScopesOperation,
- UiBridgeListSnapshotsOperation,
- UiBridgeListStateSurfacesOperation,
- UiBridgeListWindowsOperation,
- UiBridgeMutateObjectStateFieldOperation,
- UiBridgeNavigateWindowOperation,
- UiBridgeOperation,
- UiBridgeOperationServer,
- UiBridgeRestoreSnapshotOperation,
UiBridgeSelectedPlateWorkflowOperation,
UiBridgeSessionEventsOperation,
- UiBridgeSnapshotWindowOperation,
- UiBridgeStatusOperation,
- UiBridgeSwitchBranchOperation,
- UiBridgeTimeTravelHeadOperation,
- UiBridgeValidateDocumentOperation,
- UiBridgeWidgetTreeOperation,
- serves,
)
-from openhcs.agent.ui_bridge_identities import (
- PlateManagerOrchestratorCodeDocumentIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
- PlateManagerWidgetIdentity,
-)
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- CONFIRMATION_REQUIRED_GUARD,
- RESTORE_TIME_TRAVEL_OPT_IN_GUARD,
- UiActionProviderABC,
- UiBridgeGuardPolicy,
- UiBridgeSnapshotProviderABC,
- UiCodeDocumentProviderABC,
- UiLiveOverviewContributorABC,
- UiLiveOverviewContributorIdentity,
- UiObjectStateScopeProviderABC,
- UiStateSurfaceProviderABC,
- UiWindowProviderABC,
+from openhcs.agent.services.function_catalog_service import (
+ FunctionCatalogServiceABC,
+ UnknownFunctionIdError,
)
-from openhcs.pyqt_gui.services.ui_bridge_object_state_scope_policy import (
- ObjectStateScopeVisibility,
+from openhcs.core.function_reference import (
+ FunctionReference,
+ RegistryFunctionReference,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
- UiBridgeProviderSetABC,
- UiBridgeRegistrationContext,
- UiBridgeSurfaceRegistry,
+from openhcs.agent.ui_bridge_identities import (
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+ DatasetListWidgetIdentity,
)
from openhcs.ui.shared.plate_manager_code_document import (
PlateManagerCodeDocumentAuthority,
@@ -169,19 +47,20 @@
PlateManagerDocumentAssignments,
PlateManagerOrchestratorCodePayload,
)
+from formbridge.bridge.in_process import UiCodeDocumentValidationError
if TYPE_CHECKING:
from openhcs.authoring.session.session import Session
-UiBridgeObservedResultT = TypeVar("UiBridgeObservedResultT", bound=AgentResultEnvelope)
-
_ORCHESTRATOR_CODE_DOCUMENT_EXAMPLE = PlateManagerDocumentAssignments(
plate_paths="['/path/to/plate']",
global_config="GlobalPipelineConfig(...)",
per_plate_configs="{'/path/to/plate': PipelineConfig(...)}",
pipeline_data="{'/path/to/plate': [FunctionStep(...), ...]}",
)
+
+
ORCHESTRATOR_CODE_DOCUMENT_PAYLOAD_HINT = (
"Use the plate-manager orchestrator code document shape: "
+ "; ".join(
@@ -189,10 +68,11 @@
for name in PlateManagerDocumentAssignments.names()
)
+ ". "
- f"Read {PlateManagerOrchestratorCodeDocumentIdentity.require_value()} "
+ f"Read {DatasetListCodeDocumentIdentity.require_value()} "
"for the current template."
)
+
@dataclass(frozen=True, slots=True)
class CodeDocumentExecutionResult(PlateManagerOrchestratorCodePayload):
"""Parsed and normalized payload from one declarative UI code document."""
@@ -649,932 +529,18 @@ def validate_source(self, source: str, operations) -> CodeDocumentExecutionResul
return CodeDocumentExecutionResult.from_payload(payload)
-@dataclass(slots=True)
-class UiCodeDocumentValidationError(ValueError):
- errors: tuple[AgentError, ...]
-
- def __str__(self) -> str:
- return "; ".join(error.message for error in self.errors)
-
-
-class UiBridgeBusyError(RuntimeError):
- """Raised when a mutating UI bridge operation is already running."""
-
-
-class UiBridgeMutationGate:
- """Serialize mutating bridge operations for one running UI."""
-
- def __init__(self, tracker: "UiBridgeOperationTracker") -> None:
- self._lock = threading.Lock()
- self._tracker = tracker
-
- def run(
- self,
- *,
- operation_type: type[UiBridgeOperation],
- target_id: str | None,
- callback: Callable[
- [UiBridgeOperationRef],
- AgentResultEnvelope,
- ],
- ):
- operation = self.begin(operation_type=operation_type, target_id=target_id)
- try:
- result = callback(operation)
- self.complete(operation, operation_type, result)
- return result
- except Exception as exc:
- self.fail(operation, exc)
- raise
-
- def begin(
- self,
- *,
- operation_type: type[UiBridgeOperation],
- target_id: str | None,
- ) -> UiBridgeOperationRef:
- """Start one serialized mutation and return its pollable operation ref."""
- if not self._lock.acquire(blocking=False):
- raise UiBridgeBusyError(
- "A mutating UI bridge operation is already running."
- )
- return self._tracker.start(operation_type.require_name(), target_id)
-
- def complete(
- self,
- operation: UiBridgeOperationRef,
- operation_type: type[UiBridgeOperation],
- result: AgentResultEnvelope,
- ) -> None:
- """Complete a mutation successfully and release the mutation gate."""
- try:
- self._tracker.complete(
- operation.identity.operation_id,
- status=OperationCompleted,
- outcome=operation_type.outcome(result),
- errors=result.errors,
- warnings=result.warnings,
- )
- finally:
- self._lock.release()
-
- def fail(
- self,
- operation: UiBridgeOperationRef,
- exception: Exception,
- ) -> None:
- """Complete a mutation as failed and release the mutation gate."""
- try:
- self._tracker.complete(
- operation.identity.operation_id,
- status=OperationFailed,
- outcome="error",
- errors=(
- AgentError.from_exception("ui_bridge_operation_failed", exception),
- ),
- )
- finally:
- self._lock.release()
-
-
-class UiBridgeOperationTracker(UiLiveOverviewContributorABC):
- """In-memory operation status projection for bridge calls."""
-
- overview_identity = UiLiveOverviewContributorIdentity(
- section_id="ui-bridge-operations",
- title="UI bridge operations",
- )
-
- def __init__(self) -> None:
- self._operations: dict[str, UiBridgeOperationRef] = {}
- self._lock = threading.Lock()
-
- def start(self, operation_name: str, target_id: str | None) -> UiBridgeOperationRef:
- operation_id = str(uuid.uuid4())
- operation = UiBridgeOperationRef(
- schema_version=SCHEMA_VERSION,
- identity=UiBridgeOperationIdentity(
- operation_id=operation_id,
- route=UiBridgeOperationRoute(
- operation_name=operation_name,
- target_id=target_id,
- ),
- ),
- status=OperationRunning.name,
- started_at_unix=time.time(),
- )
- with self._lock:
- self._operations[operation_id] = operation
- return operation
-
- def complete(
- self,
- operation_id: str,
- *,
- status: type[UiBridgeOperationStatus],
- outcome: str | None,
- errors: tuple[AgentError, ...] = (),
- warnings: tuple[AgentWarning, ...] = (),
- result: AgentResultEnvelope | None = None,
- ) -> UiBridgeOperationRef:
- with self._lock:
- operation = self._operations[operation_id]
- completed = replace(
- operation,
- status=status.name,
- completed_at_unix=time.time(),
- outcome=outcome,
- errors=errors,
- warnings=warnings,
- result_payload=None if result is None else to_jsonable(result),
- )
- self._operations[operation_id] = completed
- return completed
-
- def complete_result(
- self, operation_id: str, result: AgentResultEnvelope
- ) -> UiBridgeOperationRef:
- """Project one typed completion through its registered declaration owner."""
- operation = self.get(operation_id)
- contract = UiBridgeOperation.for_name(
- operation.identity.operation_name
- )
- if not isinstance(result, contract.result_type):
- raise TypeError(
- f"{contract.require_name()} requires {contract.result_type.__qualname__}."
- )
- return self.complete(
- operation_id,
- status=(
- OperationFailed
- if result.errors
- else OperationCompleted
- ),
- outcome="error" if result.errors else contract.success_outcome,
- errors=result.errors,
- warnings=result.warnings,
- result=result,
- )
-
- def fail_exception(
- self, operation_id: str, error: Exception
- ) -> UiBridgeOperationRef:
- operation = self.get(operation_id)
- contract = UiBridgeOperation.for_name(
- operation.identity.operation_name
- )
- return self.complete(
- operation_id,
- status=OperationFailed,
- outcome="error",
- errors=(AgentError.from_exception(contract.failure_error_code, error),),
- )
-
- def observe(
- self,
- contract: type[UiBridgeOperation],
- target_id: str,
- invoke: Callable[
- [Callable[[UiBridgeObservedResultT], None]], UiBridgeObservedResultT
- ],
- ) -> tuple[UiBridgeOperationRef, UiBridgeObservedResultT]:
- """Use this tracker for one asynchronously completed typed operation."""
- operation = self.start(contract.require_name(), target_id)
- try:
- result = invoke(
- partial(self.complete_result, operation.identity.operation_id)
- )
- if (
- result.errors
- and self.get(operation.identity.operation_id).completed_at_unix is None
- ):
- self.complete_result(operation.identity.operation_id, result)
- except Exception as exc:
- self.fail_exception(operation.identity.operation_id, exc)
- raise
- return operation, result
-
- def get(self, operation_id: str) -> UiBridgeOperationRef:
- with self._lock:
- if operation_id in self._operations:
- return self._operations[operation_id]
- return UiBridgeOperationRef(
- schema_version=SCHEMA_VERSION,
- identity=UiBridgeOperationIdentity(
- operation_id=operation_id,
- route=UNKNOWN_UI_BRIDGE_OPERATION_ROUTE,
- ),
- status=OperationNotFound.name,
- started_at_unix=0.0,
- completed_at_unix=0.0,
- outcome="not_found",
- errors=(
- AgentError(
- code="unknown_ui_bridge_operation",
- message=f"Unknown UI bridge operation: {operation_id}",
- ),
- ),
- )
-
- def operations(self) -> tuple[UiBridgeOperationRef, ...]:
- with self._lock:
- return tuple(self._operations.values())
-
- def overview_sections(self) -> tuple[UiLiveOverviewSection, ...]:
- operations = self.operations()
- status_counts = {
- status: sum(1 for operation in operations if operation.status == status.name)
- for status in (OperationRunning, OperationFailed, OperationCompleted)
- }
- recent_operations = tuple(reversed(operations[-5:]))
- return (
- UiLiveOverviewSection(
- section_id=self.overview_identity.section_id,
- title=self.overview_identity.title,
- summary=(
- f"{status_counts[OperationRunning]} running, "
- f"{status_counts[OperationFailed]} failed, "
- f"{status_counts[OperationCompleted]} completed"
- ),
- metrics=(
- UiLiveOverviewMetric(
- key=OperationRunning.name,
- label="Running",
- value=str(status_counts[OperationRunning]),
- ),
- UiLiveOverviewMetric(
- key=OperationFailed.name,
- label="Failed",
- value=str(status_counts[OperationFailed]),
- ),
- UiLiveOverviewMetric(
- key=OperationCompleted.name,
- label="Completed",
- value=str(status_counts[OperationCompleted]),
- ),
- ),
- items=tuple(
- UiLiveOverviewItem(
- label=operation.identity.operation_name,
- status=operation.status,
- detail=self._operation_detail(operation),
- severity=self._operation_severity(operation),
- )
- for operation in recent_operations
- ),
- ),
- )
-
- @staticmethod
- def _operation_status(
- operation: UiBridgeOperationRef,
- ) -> type[UiBridgeOperationStatus]:
- try:
- return UiBridgeOperationStatus.named(operation.status)
- except ValueError:
- return OperationUnavailable
-
- @classmethod
- def _operation_severity(cls, operation: UiBridgeOperationRef) -> str:
- if operation.errors:
- return UiLiveOverviewSeverity.ERROR.value
- if operation.warnings:
- return UiLiveOverviewSeverity.WARNING.value
- return cls._operation_status(operation).live_overview_severity
-
- @staticmethod
- def _operation_detail(operation: UiBridgeOperationRef) -> str:
- route = operation.identity.route
- if route.target_id is None:
- return operation.identity.operation_id
- return f"{route.target_id} ({operation.identity.operation_id})"
-
-
-@dataclass(slots=True)
-class UiBridgePostedMutation:
- """Completion box for a mutation that may run inline or on the Qt event loop."""
-
- result: AgentResultEnvelope | None = None
- error: Exception | None = None
-
- @property
- def completed(self) -> bool:
- return self.result is not None or self.error is not None
-
- def complete(self, result: AgentResultEnvelope) -> None:
- self.result = result
-
- def fail(self, error: Exception) -> None:
- self.error = error
-
- def require_result(self) -> AgentResultEnvelope:
- if self.error is not None:
- raise self.error
- if self.result is None:
- raise RuntimeError("Queued UI bridge mutation has not completed.")
- return self.result
-
-
-class UiObjectStateSnapshotProvider(UiBridgeSnapshotProviderABC):
- """Typed projection and restore API for ObjectState history."""
-
- def __init__(
- self,
- before_restore: Callable[[], None] | None = None,
- ) -> None:
- self._before_restore = before_restore
-
- def list_snapshots(
- self,
- request: UiSnapshotListRequest,
- ) -> UiSnapshotCatalog:
- return self.catalog(request)
-
- def catalog(
- self,
- visibility_request: UiObjectStateScopeVisibility | None = None,
- ) -> UiSnapshotCatalog:
- visibility = ObjectStateScopeVisibility(
- visibility_request or UiObjectStateScopeVisibility()
- )
- history = ObjectStateRegistry.get_branch_history()
- current_branch = ObjectStateRegistry.get_current_branch()
- current_index = self.current_snapshot_index()
- refs = tuple(
- self._snapshot_ref(snapshot, index, history, visibility)
- for index, snapshot in enumerate(history)
- if visibility.includes_system_scopes()
- or self._visible_state_count(snapshot) > 0
- )
- return UiSnapshotCatalog(
- schema_version=SCHEMA_VERSION,
- current_branch=current_branch,
- current_snapshot_index=current_index,
- object_state_token=ObjectStateRegistry.get_token(),
- active=ObjectStateRegistry.is_time_traveling(),
- snapshots=refs,
- branches=self.branch_refs(),
- )
-
- def branch_refs(self) -> tuple[UiBranchRef, ...]:
- branches = []
- for branch in ObjectStateRegistry.list_branches():
- branches.append(
- UiBranchRef(
- schema_version=SCHEMA_VERSION,
- name=str(branch["name"]),
- head_snapshot_id=str(branch["head_id"]),
- base_snapshot_id=str(branch["base_id"]),
- description=str(branch["description"]),
- is_current=bool(branch["is_current"]),
- )
- )
- return tuple(branches)
-
- def current_snapshot(self) -> UiSnapshotRef | None:
- history = ObjectStateRegistry.get_branch_history()
- if not history:
- return None
- current_index = self.current_snapshot_index()
- if current_index == -1:
- return None
- return self._snapshot_ref(
- history[current_index],
- current_index,
- history,
- ObjectStateScopeVisibility(
- UiObjectStateScopeVisibility(include_system_scopes=True)
- ),
- )
-
- def current_snapshot_index(self) -> int:
- current_index = ObjectStateRegistry.get_current_snapshot_index()
- if current_index != -1:
- return current_index
- history = ObjectStateRegistry.get_branch_history()
- if not history:
- return -1
- return len(history) - 1
-
- def current_branch_head_snapshot_id(self) -> str | None:
- history = ObjectStateRegistry.get_branch_history()
- if not history:
- return None
- return history[-1].id
-
- def revision_token(self, document_id: str) -> str:
- time_travel_active = ObjectStateRegistry.is_time_traveling()
- parts = (
- document_id,
- str(ObjectStateRegistry.get_token()),
- ObjectStateRegistry.get_current_branch(),
- str(self.current_branch_head_snapshot_id()),
- str(ObjectStateRegistry.get_current_snapshot_index()),
- str(time_travel_active),
- )
- return hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()
-
- def restore_snapshot(
- self,
- request: UiSnapshotRestoreRequest,
- ) -> UiSnapshotRestoreResult:
- guard_error = self._restore_guard_policy(request).first_error()
- if guard_error is not None:
- return self._restore_error(guard_error)
-
- target = self._target_snapshot(request)
- if target is None:
- return self._restore_error(
- AgentError(
- code="snapshot_not_found",
- message="Snapshot target was not found.",
- )
- )
-
- if request.snapshot_id is not None:
- operation = partial(
- ObjectStateRegistry.time_travel_to_snapshot,
- request.snapshot_id,
- )
- elif request.index is not None:
- operation = partial(
- ObjectStateRegistry.time_travel_to,
- request.index,
- )
- elif request.branch is not None:
- operation = partial(self._restore_branch_head, request.branch)
- else:
- operation = bool
- restored, restore_error = self._run_restore(operation)
- if restore_error is not None:
- return self._restore_error(restore_error)
-
- return UiSnapshotRestoreResult(
- schema_version=SCHEMA_VERSION,
- restored=restored,
- target_snapshot=target,
- current_snapshot=self.current_snapshot(),
- catalog=self.catalog(request),
- )
-
- def time_travel_head(
- self,
- request: UiTimeTravelHeadRequest,
- ) -> UiSnapshotRestoreResult:
- if request.confirmation_is_required():
- return self._restore_error(
- AgentError(
- code="confirmation_required",
- message="UI confirmation is required.",
- )
- )
- target = self.current_snapshot()
- restored, restore_error = self._run_restore(
- ObjectStateRegistry.time_travel_to_head
- )
- if restore_error is not None:
- return self._restore_error(restore_error)
- return UiSnapshotRestoreResult(
- schema_version=SCHEMA_VERSION,
- restored=restored,
- target_snapshot=target,
- current_snapshot=self.current_snapshot(),
- catalog=self.catalog(),
- )
-
- def switch_branch(
- self,
- request: UiBranchSwitchRequest,
- ) -> UiSnapshotRestoreResult:
- return self.restore_snapshot(request.as_restore_request())
-
- def _run_restore(
- self,
- operation: Callable[[], bool],
- ) -> tuple[bool, AgentError | None]:
- """Authorize and execute one ObjectState history mutation."""
-
- try:
- if self._before_restore is not None:
- self._before_restore()
- return bool(operation()), None
- except Exception as exc:
- return False, AgentError.from_exception(
- "ui_snapshot_restore_rejected",
- exc,
- )
-
- def _target_snapshot(
- self,
- request: UiSnapshotRestoreRequest,
- ) -> UiSnapshotRef | None:
- if request.snapshot_id is not None:
- return self._ref_for_snapshot_id(request.snapshot_id)
- if request.index is not None:
- history = ObjectStateRegistry.get_branch_history()
- if request.index < 0:
- index = len(history) + request.index
- else:
- index = request.index
- if 0 <= index < len(history):
- return self._snapshot_ref(
- history[index],
- index,
- history,
- ObjectStateScopeVisibility(
- UiObjectStateScopeVisibility(include_system_scopes=True)
- ),
- )
- return None
- if request.branch is not None:
- branches = {branch.name: branch for branch in self.branch_refs()}
- if request.branch not in branches:
- return None
- return self._ref_for_snapshot_id(
- branches[request.branch].head_snapshot_id,
- branch=request.branch,
- )
- return None
-
- def _restore_branch_head(self, branch: str) -> bool:
- known_branches = {ref.name for ref in self.branch_refs()}
- if branch not in known_branches:
- return False
- return ObjectStateRegistry.switch_branch(branch)
-
- def _ref_for_snapshot_id(
- self,
- snapshot_id: str,
- *,
- branch: str | None = None,
- ) -> UiSnapshotRef | None:
- history = ObjectStateRegistry.get_branch_history(branch)
- for index, snapshot in enumerate(history):
- if snapshot.id == snapshot_id:
- return self._snapshot_ref(
- snapshot,
- index,
- history,
- ObjectStateScopeVisibility(
- UiObjectStateScopeVisibility(include_system_scopes=True)
- ),
- )
- return None
-
- def _snapshot_ref(
- self,
- snapshot,
- index: int,
- history,
- visibility: ObjectStateScopeVisibility,
- ) -> UiSnapshotRef:
- current_index = ObjectStateRegistry.get_current_snapshot_index()
- head_index = len(history) - 1
- is_head = index == head_index
- is_current = (current_index == -1 and is_head) or current_index == index
- return UiSnapshotRef(
- schema_version=SCHEMA_VERSION,
- snapshot_id=snapshot.id,
- index=index,
- branch=ObjectStateRegistry.get_current_branch(),
- parent_snapshot_id=snapshot.parent_id,
- timestamp_unix=snapshot.timestamp,
- timestamp=_datetime.datetime.fromtimestamp(snapshot.timestamp).isoformat(
- timespec="milliseconds"
- ),
- label=snapshot.label or f"Snapshot #{index}",
- num_states=(
- len(snapshot.all_states)
- if visibility.includes_system_scopes()
- else self._visible_state_count(snapshot)
- ),
- is_current=is_current,
- is_head=is_head,
- uri=f"openhcs://ui/snapshots/{snapshot.id}",
- )
-
- @staticmethod
- def _visible_state_count(snapshot) -> int:
- visibility = ObjectStateScopeVisibility()
- return sum(
- 1
- for scope_id in snapshot.all_states
- if visibility.includes_scope_id(scope_id)
- )
-
- @staticmethod
- def _restore_guard_policy(
- request: UiSnapshotRestoreRequest,
- ) -> UiBridgeGuardPolicy:
- return UiBridgeGuardPolicy(
- rules=(
- CONFIRMATION_REQUIRED_GUARD.bind(
- lambda: request.confirmation_is_required(),
- ),
- RESTORE_TIME_TRAVEL_OPT_IN_GUARD.bind(
- lambda: (
- ObjectStateRegistry.is_time_traveling()
- and not request.allow_auto_branch
- ),
- ),
- )
- )
-
- @staticmethod
- def _restore_error(error: AgentError) -> UiSnapshotRestoreResult:
- return UiSnapshotRestoreResult(
- schema_version=SCHEMA_VERSION,
- restored=False,
- target_snapshot=None,
- current_snapshot=None,
- catalog=None,
- errors=(error,),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiCodeDocumentApplyLabel:
- """Formal defaulting authority for apply snapshot labels."""
-
- value: str
-
- @classmethod
- def resolve(
- cls,
- request: UiCodeDocumentApplyRequest,
- identity: UiCodeDocumentIdentity,
- ) -> "UiCodeDocumentApplyLabel":
- if request.snapshot_label is not None:
- return cls(request.snapshot_label)
- return cls(identity.default_edit_label)
-
-
-class UiAgentBridgeService(UiBridgeOperationServer):
- """In-process UI bridge owned by the running PyQt main window."""
+class OpenHCSUiAgentBridgeService(UiAgentBridgeService):
+ """The OpenHCS GUI's in-process bridge: the generic operations plus its session's."""
def __init__(
self,
*,
- provider_set: UiBridgeProviderSetABC | None = None,
- registry: UiBridgeSurfaceRegistry | None = None,
- dispatcher: UiThreadDispatcher | None = None,
- snapshot_provider: UiObjectStateSnapshotProvider | None = None,
- operation_tracker: UiBridgeOperationTracker | None = None,
- object_state_mutation_authorizer: Callable[[str], None] | None = None,
session: "Session | None" = None,
+ dispatcher: MainThreadDispatcher | None = None,
+ **kwargs,
) -> None:
+ super().__init__(dispatcher=dispatcher or UiThreadDispatcher(), **kwargs)
self._session = session
- self._dispatcher = dispatcher or UiThreadDispatcher()
- self._snapshot_provider = snapshot_provider or UiObjectStateSnapshotProvider()
- self._operation_tracker = operation_tracker or UiBridgeOperationTracker()
- self._mutation_gate = UiBridgeMutationGate(self._operation_tracker)
- self._object_state_mutation_authorizer = object_state_mutation_authorizer
- self._registry = registry or UiBridgeSurfaceRegistry()
- if provider_set is not None:
- provider_set.register(
- UiBridgeRegistrationContext(
- registry=self._registry,
- snapshot_provider=self._snapshot_provider,
- )
- )
-
- def close(self) -> None:
- """Fence callbacks queued by this bridge instance."""
-
- self._dispatcher.close()
-
- def register_provider(self, provider: UiCodeDocumentProviderABC) -> None:
- self._registry.register_code_document_provider(provider)
-
- def register_state_provider(self, provider: UiStateSurfaceProviderABC) -> None:
- self._registry.register_state_surface_provider(provider)
-
- def register_action_provider(self, provider: UiActionProviderABC) -> None:
- self._registry.register_action_provider(provider)
-
- def register_window_provider(self, provider: UiWindowProviderABC) -> None:
- self._registry.register_window_provider(provider)
-
- def register_object_state_scope_provider(
- self,
- provider: UiObjectStateScopeProviderABC,
- ) -> None:
- self._registry.register_object_state_scope_provider(provider)
-
- def _submit_mutation(
- self,
- *,
- operation_type: type[UiBridgeOperation],
- target_id: str | None,
- request: object,
- callback: Callable[[UiBridgeOperationRef], AgentResultEnvelope],
- ) -> AgentResultEnvelope:
- operation = self._mutation_gate.begin(
- operation_type=operation_type,
- target_id=target_id,
- )
- submitted = UiBridgePostedMutation()
-
- def run() -> None:
- try:
- result = callback(operation)
- self._mutation_gate.complete(operation, operation_type, result)
- submitted.complete(result)
- except Exception as exc:
- self._mutation_gate.fail(operation, exc)
- submitted.fail(exc)
-
- try:
- self._dispatcher.post(run)
- except Exception as exc:
- self._mutation_gate.fail(operation, exc)
- raise
-
- if submitted.completed:
- return submitted.require_result()
- return operation_type.accepted(request, operation)
-
- @serves(UiBridgeStatusOperation)
- def status(self) -> UiBridgeStatus:
- return UiBridgeStatus(
- schema_version=SCHEMA_VERSION,
- reachable=True,
- auth_required=False,
- )
-
- @serves(UiBridgeListDocumentsOperation)
- def list_documents(self) -> UiCodeDocumentCatalog:
- return self._dispatcher.call(
- lambda: UiCodeDocumentCatalog(
- schema_version=SCHEMA_VERSION,
- documents=tuple(
- summary
- for provider in self._registry.code_document_providers()
- for summary in provider.summaries()
- ),
- )
- )
-
- @serves(UiBridgeListStateSurfacesOperation)
- def list_state_surfaces(self) -> UiStateSurfaceCatalog:
- return self._dispatcher.call(
- lambda: UiStateSurfaceCatalog(
- schema_version=SCHEMA_VERSION,
- surfaces=tuple(
- provider.summary()
- for provider in self._registry.state_surface_providers()
- ),
- )
- )
-
- @serves(UiBridgeListActionsOperation)
- def list_actions(self) -> UiActionCatalog:
- def catalog() -> UiActionCatalog:
- actions = []
- errors = []
- warnings = []
- for provider in self._registry.action_providers():
- provider_catalog = provider.catalog()
- actions.extend(provider_catalog.actions)
- errors.extend(provider_catalog.errors)
- warnings.extend(provider_catalog.warnings)
- return UiActionCatalog(
- schema_version=SCHEMA_VERSION,
- actions=tuple(actions),
- errors=tuple(errors),
- warnings=tuple(warnings),
- )
-
- return self._dispatcher.call(catalog)
-
- @serves(UiBridgeListWindowsOperation)
- def list_windows(self) -> UiWindowCatalog:
- def catalog() -> UiWindowCatalog:
- windows = []
- errors = []
- warnings = []
- for provider in self._registry.window_providers():
- provider_catalog = provider.catalog()
- windows.extend(provider_catalog.windows)
- errors.extend(provider_catalog.errors)
- warnings.extend(provider_catalog.warnings)
- return UiWindowCatalog(
- schema_version=SCHEMA_VERSION,
- windows=tuple(windows),
- errors=tuple(errors),
- warnings=tuple(warnings),
- )
-
- return self._dispatcher.call(catalog)
-
- @serves(UiBridgeListObjectStateScopesOperation)
- def list_object_state_scopes(
- self,
- request: UiObjectStateScopeListRequest,
- ) -> UiObjectStateScopeCatalog:
- def catalog() -> UiObjectStateScopeCatalog:
- scopes = []
- errors = []
- warnings = []
- for provider in self._registry.object_state_scope_providers():
- provider_catalog = provider.catalog(request)
- scopes.extend(provider_catalog.scopes)
- errors.extend(provider_catalog.errors)
- warnings.extend(provider_catalog.warnings)
- return UiObjectStateScopeCatalog(
- schema_version=SCHEMA_VERSION,
- object_state_token=ObjectStateRegistry.get_token(),
- current_branch=ObjectStateRegistry.get_current_branch(),
- current_snapshot_index=self._snapshot_provider.current_snapshot_index(),
- active=ObjectStateRegistry.is_time_traveling(),
- scopes=tuple(scopes),
- errors=tuple(errors),
- warnings=tuple(warnings),
- )
-
- return self._dispatcher.call(catalog)
-
- @serves(UiBridgeMutateObjectStateFieldOperation)
- def mutate_object_state_field(
- self,
- request: UiObjectStateFieldMutationRequest,
- ) -> UiObjectStateFieldMutationResult:
- def run(operation: UiBridgeOperationRef) -> UiObjectStateFieldMutationResult:
- from openhcs.pyqt_gui.services.ui_bridge_object_state import (
- ObjectStateFieldMutationService,
- )
-
- result = ObjectStateFieldMutationService(
- before_mutation=self._object_state_mutation_authorizer,
- ).mutate(request)
- return replace(
- result,
- receipt=replace(
- result.receipt,
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- try:
- return self._dispatcher.call(
- lambda: self._mutation_gate.run(
- operation_type=UiBridgeMutateObjectStateFieldOperation,
- target_id=(f"{request.object_state_scope_id}:{request.field_path}"),
- callback=run,
- )
- )
- except UiBridgeBusyError as exc:
- return UiObjectStateFieldMutationResult(
- schema_version=SCHEMA_VERSION,
- address=request,
- mutated=False,
- reset=request.reset,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=(AgentError.from_exception("ui_bridge_busy", exc),),
- )
-
- @serves(UiBridgeGetDocumentOperation)
- def get_document(self, request: UiCodeDocumentRequest) -> UiCodeDocument:
- return self._dispatcher.call(
- lambda: self._registry.code_document_provider(request.document_id).read(
- request
- )
- )
-
- @serves(UiBridgeGetStateSurfaceOperation)
- def get_state_surface(
- self, request: UiStateSurfaceRequest
- ) -> UiStateSurfaceDocument:
- return self._dispatcher.call(
- lambda: self._registry.state_surface_provider(request.surface_id).read(
- request
- )
- )
-
- @serves(UiBridgeInvokeActionOperation)
- def invoke_action(self, request: UiActionInvokeRequest) -> UiActionInvokeResult:
- def run(operation: UiBridgeOperationRef) -> UiActionInvokeResult:
- result = self._registry.action_provider(request.widget_id).invoke(request)
- return replace(
- result,
- receipt=replace(
- result.receipt,
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- try:
- return self._submit_mutation(
- operation_type=UiBridgeInvokeActionOperation,
- target_id=request.action_id,
- request=request,
- callback=run,
- )
- except UiBridgeBusyError as exc:
- return self._action_error(
- request,
- AgentError.from_exception("ui_bridge_busy", exc),
- )
@serves(UiBridgeSelectedPlateWorkflowOperation)
def selected_plate_workflow(
@@ -1583,7 +549,7 @@ def selected_plate_workflow(
) -> UiSelectedPlateWorkflowResult:
action_result = self.invoke_action(
UiActionInvokeRequest(
- widget_id=PlateManagerWidgetIdentity.require_value(),
+ widget_id=DatasetListWidgetIdentity.require_value(),
action_id=request.workflow.value,
selected_scope_ids=request.selected_scope_ids,
observed_selection_revision_token=(
@@ -1597,7 +563,7 @@ def selected_plate_workflow(
action_result = replace(
action_result,
workflow_status_surface_ids=(
- PlateManagerStateSurfaceIdentityDeclaration.require_value(),
+ DatasetListStateSurfaceIdentity.require_value(),
),
)
return UiSelectedPlateWorkflowResult(
@@ -1608,234 +574,6 @@ def selected_plate_workflow(
warnings=action_result.warnings,
)
- @serves(UiBridgeFocusWindowOperation)
- def focus_window(self, request: UiWindowFocusRequest) -> UiWindowFocusResult:
- return self._dispatcher.call(
- lambda: self._registry.window_provider(request.window_id).focus(request)
- )
-
- @serves(UiBridgeNavigateWindowOperation)
- def navigate_window(
- self,
- request: UiWindowNavigateRequest,
- ) -> UiWindowNavigateResult:
- def navigate() -> UiWindowNavigateResult:
- provider = self._registry.window_provider(request.window_id)
- if not request.has_target:
- return provider.navigate(request)
- operation, result = self._operation_tracker.observe(
- UiBridgeNavigateWindowOperation,
- request.window_id,
- partial(provider.navigate, request),
- )
- # Accepted target dispatch is not terminal execution or exposure.
- return replace(
- result,
- operation_id=operation.identity.operation_id,
- navigated=False,
- target_exposed=None,
- )
-
- return self._dispatcher.call(navigate)
-
- @serves(UiBridgeCloseWindowOperation)
- def close_window(self, request: UiWindowCloseRequest) -> UiWindowCloseResult:
- return self._dispatcher.call(
- lambda: self._registry.window_provider(request.window_id).close(request)
- )
-
- @serves(UiBridgeSnapshotWindowOperation)
- def snapshot_window(
- self,
- request: UiWindowSnapshotRequest,
- ) -> UiWindowSnapshotResult:
- def capture() -> UiWindowSnapshotResult:
- provider = self._registry.window_provider(request.window_id)
- if not request.frame_condition.observes:
- return provider.snapshot(request)
- operation, result = self._operation_tracker.observe(
- UiBridgeSnapshotWindowOperation,
- request.window_id,
- partial(provider.snapshot, request),
- )
- return project_dataclass(
- UiWindowSnapshotResult,
- result,
- operation_id=operation.identity.operation_id,
- )
-
- return self._dispatcher.call(capture)
-
- @serves(UiBridgeWidgetTreeOperation)
- def widget_tree(
- self,
- request: UiWidgetTreeRequest,
- ) -> UiWidgetTreeResult:
- return self._dispatcher.call(
- lambda: self._registry.window_provider(request.window_id).widget_tree(
- request
- )
- )
-
- @serves(UiBridgeInvokeWidgetActionOperation)
- def invoke_widget_action(
- self,
- request: UiWidgetActionInvokeRequest,
- ) -> UiWidgetActionInvokeResult:
- def run(operation: UiBridgeOperationRef) -> UiWidgetActionInvokeResult:
- provider = self._registry.window_provider(request.window_id)
- result = provider.invoke_widget_action(request)
- return replace(
- result,
- receipt=replace(
- result.receipt,
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- try:
- return self._submit_mutation(
- operation_type=UiBridgeInvokeWidgetActionOperation,
- target_id=f"{request.window_id}:{request.path_id}",
- request=request,
- callback=run,
- )
- except UiBridgeBusyError as exc:
- return self._widget_action_error(
- request,
- AgentError.from_exception("ui_bridge_busy", exc),
- )
-
- @serves(UiBridgeValidateDocumentOperation)
- def validate_document(
- self,
- request: UiCodeDocumentValidationRequest,
- ) -> UiCodeDocumentValidationResult:
- return self._dispatcher.call(
- lambda: self._registry.code_document_provider(request.document_id).validate(
- request
- )
- )
-
- @serves(UiBridgeApplyDocumentOperation)
- def apply_document(
- self,
- request: UiCodeDocumentApplyRequest,
- ) -> UiCodeDocumentApplyResult:
- def run(operation: UiBridgeOperationRef) -> UiCodeDocumentApplyResult:
- result = self._registry.code_document_provider(request.document_id).apply(
- request
- )
- return replace(
- result,
- operation_id=operation.identity.operation_id,
- receipt=replace(
- result.receipt,
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- try:
- return self._submit_mutation(
- operation_type=UiBridgeApplyDocumentOperation,
- target_id=request.document_id,
- request=request,
- callback=run,
- )
- except UiBridgeBusyError as exc:
- return self._apply_document_error(
- request,
- AgentError.from_exception("ui_bridge_busy", exc),
- )
-
- @serves(UiBridgeListSnapshotsOperation)
- def list_snapshots(self, request: UiSnapshotListRequest) -> UiSnapshotCatalog:
- return self._dispatcher.call(
- lambda: self._snapshot_provider.list_snapshots(request)
- )
-
- @serves(UiBridgeRestoreSnapshotOperation)
- def restore_snapshot(
- self,
- request: UiSnapshotRestoreRequest,
- ) -> UiSnapshotRestoreResult:
- def run(operation: UiBridgeOperationRef) -> UiSnapshotRestoreResult:
- result = self._snapshot_provider.restore_snapshot(request)
- return replace(
- result,
- operation_id=operation.identity.operation_id,
- receipt=UiMutationReceipt.accepted_for(
- UiMutationRequestToken(),
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- return self._submit_mutation(
- operation_type=UiBridgeRestoreSnapshotOperation,
- target_id=request.operation_target_id,
- request=request,
- callback=run,
- )
-
- @serves(UiBridgeTimeTravelHeadOperation)
- def time_travel_head(
- self,
- request: UiTimeTravelHeadRequest,
- ) -> UiSnapshotRestoreResult:
- def run(operation: UiBridgeOperationRef) -> UiSnapshotRestoreResult:
- result = self._snapshot_provider.time_travel_head(request)
- return replace(
- result,
- operation_id=operation.identity.operation_id,
- receipt=UiMutationReceipt.accepted_for(
- UiMutationRequestToken(),
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- return self._submit_mutation(
- operation_type=UiBridgeTimeTravelHeadOperation,
- target_id=None,
- request=request,
- callback=run,
- )
-
- @serves(UiBridgeListBranchesOperation)
- def list_branches(self) -> UiBranchCatalog:
- return self._dispatcher.call(
- lambda: UiBranchCatalog(
- schema_version=SCHEMA_VERSION,
- current_branch=ObjectStateRegistry.get_current_branch(),
- branches=self._snapshot_provider.branch_refs(),
- )
- )
-
- @serves(UiBridgeSwitchBranchOperation)
- def switch_branch(self, request: UiBranchSwitchRequest) -> UiSnapshotRestoreResult:
- def run(operation: UiBridgeOperationRef) -> UiSnapshotRestoreResult:
- result = self._snapshot_provider.switch_branch(request)
- return replace(
- result,
- operation_id=operation.identity.operation_id,
- receipt=UiMutationReceipt.accepted_for(
- UiMutationRequestToken(),
- bridge_operation_id=operation.identity.operation_id,
- ),
- )
-
- return self._submit_mutation(
- operation_type=UiBridgeSwitchBranchOperation,
- target_id=request.branch,
- request=request,
- callback=run,
- )
-
- @serves(UiBridgeGetOperationStatusOperation)
- def get_operation_status(
- self, request: UiBridgeOperationStatusRequest
- ) -> UiBridgeOperationRef:
- return self._operation_tracker.get(request.operation_id)
-
@serves(UiBridgeSessionEventsOperation)
def session_events(self, request: SessionEventsRequest) -> SessionEventBatch:
"""The hosting session's events; the wait runs off the UI thread."""
@@ -1857,60 +595,3 @@ def session_events(self, request: SessionEventsRequest) -> SessionEventBatch:
last_sequence=self._session.event_log.last_sequence,
)
- @staticmethod
- def _apply_document_error(
- request: UiCodeDocumentApplyRequest,
- error: AgentError,
- ) -> UiCodeDocumentApplyResult:
- return UiCodeDocumentApplyResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- applied=False,
- base_revision_token=request.base_revision_token,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=(error,),
- )
-
- @staticmethod
- def _action_error(
- request: UiActionInvokeRequest,
- error: AgentError,
- ) -> UiActionInvokeResult:
- return UiActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- identity=UiActionIdentity(
- widget_id=request.widget_id,
- action_id=request.action_id,
- ),
- status=ActionRejected,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- target_scope_ids=request.selected_scope_ids,
- selection_revision_token=request.observed_selection_revision_token,
- errors=(error,),
- )
-
- @staticmethod
- def _widget_action_error(
- request: UiWidgetActionInvokeRequest,
- error: AgentError,
- ) -> UiWidgetActionInvokeResult:
- return UiWidgetActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- path_id=request.path_id,
- action_kind=request.action_kind,
- invoked=False,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- errors=(error,),
- )
-
-
-class InProcessUiBridgeGateway(UiBridgeGatewayABC):
- """Gateway adapter for tests or same-process MCP embedding."""
-
- def __init__(self, bridge: UiAgentBridgeService) -> None:
- self._bridge = bridge
-
- def invoke(self, connection: UiBridgeConnectionSpec, operation, request):
- del connection
- return self._bridge.invoke(operation, request)
diff --git a/openhcs/pyqt_gui/services/ui_bridge_composition.py b/openhcs/pyqt_gui/services/ui_bridge_composition.py
index 3ebbac7430..42bcdfd3f1 100644
--- a/openhcs/pyqt_gui/services/ui_bridge_composition.py
+++ b/openhcs/pyqt_gui/services/ui_bridge_composition.py
@@ -1,22 +1,28 @@
-"""Composition root for the PyQt UI bridge."""
+"""Composition root for the OpenHCS GUI's UI bridge."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
-from openhcs.pyqt_gui.services.ui_agent_bridge import (
- UiAgentBridgeService,
+import openhcs.pyqt_gui.services.ui_bridge_live_overview # noqa: F401 (provider set)
+import openhcs.pyqt_gui.services.ui_bridge_pipeline_editor # noqa: F401 (provider set)
+import openhcs.pyqt_gui.services.ui_bridge_plate_manager # noqa: F401 (provider set)
+import openhcs.pyqt_gui.services.ui_bridge_windows # noqa: F401 (provider set)
+import pyqt_reactive.bridge.object_state_documents # noqa: F401 (provider set)
+from formbridge.bridge.in_process import (
UiBridgeOperationTracker,
UiObjectStateSnapshotProvider,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
+from formbridge.bridge.registry import (
CompositeUiBridgeProviderSet,
UiBridgeProviderSetABC,
UiBridgeRegistrationContext,
UiBridgeSurfaceRegistry,
)
+from openhcs.pyqt_gui.services.ui_agent_bridge import OpenHCSUiAgentBridgeService
+
if TYPE_CHECKING:
from openhcs.authoring.session.session import Session
@@ -42,7 +48,7 @@ def for_main_window(cls, main_window) -> "OpenHCSUiBridgeCompositionRoot":
main_window.session,
)
- def build_service(self) -> UiAgentBridgeService:
+ def build_service(self) -> OpenHCSUiAgentBridgeService:
snapshot_provider = UiObjectStateSnapshotProvider(
before_restore=self.session.require_definition_mutation_allowed,
)
@@ -55,7 +61,7 @@ def build_service(self) -> UiAgentBridgeService:
snapshot_provider=snapshot_provider,
)
)
- return UiAgentBridgeService(
+ return OpenHCSUiAgentBridgeService(
registry=registry,
snapshot_provider=snapshot_provider,
operation_tracker=operation_tracker,
diff --git a/openhcs/pyqt_gui/services/ui_bridge_contracts.py b/openhcs/pyqt_gui/services/ui_bridge_contracts.py
deleted file mode 100644
index 24c36b3447..0000000000
--- a/openhcs/pyqt_gui/services/ui_bridge_contracts.py
+++ /dev/null
@@ -1,629 +0,0 @@
-"""Cycle-free contracts for PyQt UI bridge provider registration."""
-
-from __future__ import annotations
-
-from abc import ABC, abstractmethod
-from collections.abc import Callable, Iterable
-from dataclasses import dataclass
-from typing import ClassVar, Self
-
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
-from openhcs.agent.dto.ui_bridge import (
- ActionAccepted,
- ActionRejected,
- UiActionCatalog,
- UiActionIdentity,
- UiActionInvokeRequest,
- UiActionInvokeResult,
- UiActionSummary,
- UiCodeDocument,
- UiCodeDocumentApplyRequest,
- UiCodeDocumentApplyResult,
- UiCodeDocumentIdentity,
- UiCodeDocumentRequest,
- UiCodeDocumentSummary,
- UiCodeDocumentValidationRequest,
- UiCodeDocumentValidationResult,
- UiLiveOverviewSection,
- UiMutationReceipt,
- UiObjectStateScopeCatalog,
- UiObjectStateScopeListRequest,
- UiSnapshotRef,
- UiStateSurfaceDocument,
- UiStateSurfaceIdentity,
- UiStateSurfaceRequest,
- UiStateSurfaceSummary,
- UiWidgetActionInvokeRequest,
- UiWidgetActionInvokeResult,
- UiWidgetIdentity,
- UiWidgetTreeRequest,
- UiWidgetTreeResult,
- UiWindowCatalog,
- UiWindowCloseRequest,
- UiWindowCloseResult,
- UiWindowFocusRequest,
- UiWindowFocusResult,
- UiWindowNavigateRequest,
- UiWindowNavigateResult,
- UiWindowSnapshotRequest,
- UiWindowSnapshotResult,
-)
-from openhcs.agent.ui_bridge_identities import (
- UiCodeDocumentIdentityDeclaration,
- UiStateSurfaceIdentityDeclarationBase,
- UiWidgetIdentityDeclaration,
-)
-
-
-class UiBridgeRegistryKeyMixin:
- """Shared AutoRegisterMeta registry-key declaration for UI bridge families."""
-
- __registry_key__ = "registry_key"
- __skip_if_no_key__ = True
-
- registry_key: ClassVar[str | None] = None
-
-
-@dataclass(frozen=True, slots=True)
-class UiLiveOverviewContributorIdentity:
- """Stable identity for one live-overview contribution source."""
-
- section_id: str
- title: str
-
-
-class UiLiveOverviewContributorABC(ABC):
- """Nominal capability for providers that contribute to the live overview."""
-
- @property
- @abstractmethod
- def overview_identity(self) -> UiLiveOverviewContributorIdentity:
- raise NotImplementedError
-
- def overview_sections(self) -> tuple[UiLiveOverviewSection, ...]:
- return ()
-
-
-class UiLiveOverviewWidget:
- """Nominal widget capability for UI-owned live overview contributions."""
-
- def overview_sections(self) -> tuple[UiLiveOverviewSection, ...]:
- return ()
-
-
-class UiCodeDocumentProviderABC(ABC):
- """Provider contract for one UI-owned code document."""
-
- identity: "UiCodeDocumentProviderIdentity"
-
- def handles(self, document_id: str) -> bool:
- """Return whether this provider owns a document id."""
- return self.identity.document_id == document_id
-
- @abstractmethod
- def summary(self) -> UiCodeDocumentSummary:
- raise NotImplementedError
-
- def summaries(self) -> tuple[UiCodeDocumentSummary, ...]:
- """Return catalog summaries owned by this provider."""
- return (self.summary(),)
-
- @abstractmethod
- def read(self, request: UiCodeDocumentRequest) -> UiCodeDocument:
- raise NotImplementedError
-
- @abstractmethod
- def validate(
- self,
- request: UiCodeDocumentValidationRequest,
- ) -> UiCodeDocumentValidationResult:
- raise NotImplementedError
-
- @abstractmethod
- def apply(
- self,
- request: UiCodeDocumentApplyRequest,
- ) -> UiCodeDocumentApplyResult:
- raise NotImplementedError
-
-
-class UiStateSurfaceProviderABC(UiLiveOverviewContributorABC):
- """Provider contract for one pollable UI state surface."""
-
- identity: "UiStateSurfaceProviderIdentity"
-
- @property
- def overview_identity(self) -> UiLiveOverviewContributorIdentity:
- return UiLiveOverviewContributorIdentity(
- section_id=self.identity.revision_key,
- title=self.identity.title,
- )
-
- @abstractmethod
- def summary(self) -> UiStateSurfaceSummary:
- raise NotImplementedError
-
- @abstractmethod
- def read(self, request: UiStateSurfaceRequest) -> UiStateSurfaceDocument:
- raise NotImplementedError
-
-
-@dataclass(frozen=True, slots=True)
-class UiOwnedStateSurfaceDeclaration:
- """State-surface semantics declared by the widget that owns the state."""
-
- identity: type[UiStateSurfaceIdentityDeclarationBase]
- title: str
- payload_schema: str
- related_action_ids: tuple[str, ...] = ()
-
- @property
- def surface_id(self) -> str:
- """Project the stable protocol identity without repeating its value."""
- return self.identity.require_value()
-
- def __post_init__(self) -> None:
- if not self.title:
- raise ValueError("UI state-surface title cannot be empty.")
- if not self.payload_schema:
- raise ValueError("UI state-surface payload schema cannot be empty.")
- if any(not action_id for action_id in self.related_action_ids):
- raise ValueError("Related UI action ids cannot be empty.")
- if len(self.related_action_ids) != len(set(self.related_action_ids)):
- raise ValueError("Related UI action ids cannot contain duplicates.")
-
-
-def state_surface_ids_for_action(
- declarations: Iterable[UiOwnedStateSurfaceDeclaration],
- action_id: str,
-) -> tuple[str, ...]:
- """Project the surfaces an owner declares relevant to one action."""
- return tuple(
- declaration.surface_id
- for declaration in declarations
- if action_id in declaration.related_action_ids
- )
-
-
-def state_surface_declaration_for_identity(
- declarations: Iterable[UiOwnedStateSurfaceDeclaration],
- identity: type[UiStateSurfaceIdentityDeclarationBase],
-) -> UiOwnedStateSurfaceDeclaration:
- """Resolve exactly one owner declaration by its neutral protocol identity."""
- matches = tuple(
- declaration for declaration in declarations if declaration.identity is identity
- )
- if len(matches) != 1:
- raise ValueError(
- f"Expected exactly one UI state-surface declaration for "
- f"{identity.__name__}, found {len(matches)}."
- )
- return matches[0]
-
-
-@dataclass(frozen=True, slots=True)
-class UiActionDispatch:
- """What dispatching one UI action reported."""
-
- errors: tuple[AgentError, ...] = ()
- warnings: tuple[AgentWarning, ...] = ()
- event_sequence: int | None = None
-
-
-class UiActionProviderABC(ABC):
- """One widget's action catalog.
-
- A provider declares its action ids, the summary of each, and how to
- dispatch one; the catalog, the invocation guards (enabled, confirmation,
- targets, selection revision, all read from the summary) and the result
- are this base's.
- """
-
- identity: "UiActionProviderIdentity"
- dispatch_error_code: ClassVar[str] = "ui_action_dispatch_failed"
-
- @abstractmethod
- def action_ids(self) -> tuple[str, ...]:
- raise NotImplementedError
-
- @abstractmethod
- def summary(self, action_id: str) -> UiActionSummary:
- raise NotImplementedError
-
- @abstractmethod
- def dispatch(self, request: UiActionInvokeRequest) -> UiActionDispatch:
- raise NotImplementedError
-
- def catalog_warnings(self) -> tuple[AgentWarning, ...]:
- return ()
-
- def workflow_status_surface_ids(self) -> tuple[str, ...]:
- return ()
-
- def catalog(self) -> UiActionCatalog:
- return UiActionCatalog(
- schema_version=SCHEMA_VERSION,
- actions=tuple(self.summary(action_id) for action_id in self.action_ids()),
- warnings=self.catalog_warnings(),
- )
-
- def invoke(self, request: UiActionInvokeRequest) -> UiActionInvokeResult:
- try:
- summary = self.summary(request.action_id)
- except Exception as exc:
- return self._result(
- request,
- None,
- UiActionDispatch(
- errors=(AgentError.from_exception("unknown_ui_action", exc),)
- ),
- )
- error = summary.invocation_guard_error(request)
- if error is not None:
- return self._result(request, summary, UiActionDispatch(errors=(error,)))
- try:
- dispatched = self.dispatch(request)
- except Exception as exc:
- dispatched = UiActionDispatch(
- errors=(AgentError.from_exception(self.dispatch_error_code, exc),)
- )
- return self._result(request, summary, dispatched)
-
- def _result(
- self,
- request: UiActionInvokeRequest,
- summary: UiActionSummary | None,
- dispatched: UiActionDispatch,
- ) -> UiActionInvokeResult:
- accepted = not dispatched.errors
- return UiActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- identity=UiActionIdentity(
- widget_id=request.widget_id,
- action_id=request.action_id,
- ),
- status=ActionAccepted if accepted else ActionRejected,
- receipt=(
- UiMutationReceipt.accepted_for(request.request_token)
- if accepted
- else UiMutationReceipt.rejected_for(request.request_token)
- ),
- target_scope_ids=(
- request.selected_scope_ids
- or (() if summary is None else summary.target_scope_ids)
- ),
- selection_revision_token=(
- None if summary is None else summary.selection_revision_token
- ),
- workflow_status_surface_ids=(
- self.workflow_status_surface_ids() if accepted else ()
- ),
- event_sequence=dispatched.event_sequence,
- errors=dispatched.errors,
- warnings=tuple(dispatched.warnings),
- )
-
-
-class UiWindowProviderABC(UiLiveOverviewContributorABC):
- """Provider contract for a catalog of focusable UI windows."""
-
- identity: "UiWindowProviderIdentity"
-
- @property
- def overview_identity(self) -> UiLiveOverviewContributorIdentity:
- return UiLiveOverviewContributorIdentity(
- section_id=self.identity.revision_key,
- title=self.identity.title,
- )
-
- @abstractmethod
- def catalog(self) -> UiWindowCatalog:
- raise NotImplementedError
-
- @abstractmethod
- def handles(self, window_id: str) -> bool:
- raise NotImplementedError
-
- @abstractmethod
- def focus(self, request: UiWindowFocusRequest) -> UiWindowFocusResult:
- raise NotImplementedError
-
- @abstractmethod
- def navigate(
- self,
- request: UiWindowNavigateRequest,
- completed: Callable[[UiWindowNavigateResult], None] | None = None,
- ) -> UiWindowNavigateResult:
- raise NotImplementedError
-
- @abstractmethod
- def close(self, request: UiWindowCloseRequest) -> UiWindowCloseResult:
- raise NotImplementedError
-
- @abstractmethod
- def snapshot(
- self,
- request: UiWindowSnapshotRequest,
- completed: Callable[[UiWindowSnapshotResult], None] | None = None,
- ) -> UiWindowSnapshotResult:
- raise NotImplementedError
-
- @abstractmethod
- def widget_tree(self, request: UiWidgetTreeRequest) -> UiWidgetTreeResult:
- raise NotImplementedError
-
- @abstractmethod
- def invoke_widget_action(
- self,
- request: UiWidgetActionInvokeRequest,
- ) -> UiWidgetActionInvokeResult:
- raise NotImplementedError
-
-
-class UiObjectStateScopeProviderABC(ABC):
- """Provider contract for ObjectState registry scope projection."""
-
- identity: "UiObjectStateScopeProviderIdentity"
-
- @abstractmethod
- def catalog(
- self,
- request: UiObjectStateScopeListRequest,
- ) -> UiObjectStateScopeCatalog:
- raise NotImplementedError
-
-
-class UiBridgeSnapshotProviderABC(ABC):
- """Snapshot projection behavior required by UI bridge providers."""
-
- @abstractmethod
- def current_snapshot(self) -> UiSnapshotRef | None:
- raise NotImplementedError
-
- @abstractmethod
- def current_branch_head_snapshot_id(self) -> str | None:
- raise NotImplementedError
-
- @abstractmethod
- def revision_token(self, document_id: str) -> str:
- raise NotImplementedError
-
-
-class SnapshotBackedUiCodeDocumentProviderABC(UiCodeDocumentProviderABC):
- """Shared apply rejection behavior for snapshot-backed code documents."""
-
- _snapshot_provider: UiBridgeSnapshotProviderABC
-
- def revision_key_for_document_id(self, document_id: str) -> str:
- del document_id
- return self.identity.revision_key
-
- def _apply_error(
- self,
- request: UiCodeDocumentApplyRequest,
- error: AgentError,
- ) -> UiCodeDocumentApplyResult:
- return self._apply_errors(request, (error,))
-
- def _apply_errors(
- self,
- request: UiCodeDocumentApplyRequest,
- errors: tuple[AgentError, ...],
- ) -> UiCodeDocumentApplyResult:
- return UiCodeDocumentApplyResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- applied=False,
- base_revision_token=request.base_revision_token,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- current_revision_token=self._current_revision_token_for_apply_result(
- request
- ),
- current_snapshot=self._current_snapshot_for_apply_result(),
- errors=errors,
- )
-
- def _apply_unchanged(
- self,
- request: UiCodeDocumentApplyRequest,
- ) -> UiCodeDocumentApplyResult:
- current_revision = self._current_revision_token_for_apply_result(request)
- return UiCodeDocumentApplyResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- applied=False,
- base_revision_token=request.base_revision_token,
- receipt=UiMutationReceipt.accepted_for(request.request_token),
- outcome="unchanged",
- new_revision_token=current_revision,
- current_revision_token=current_revision,
- current_snapshot=self._current_snapshot_for_apply_result(),
- )
-
- def _current_revision_token_for_apply_result(
- self,
- request: UiCodeDocumentApplyRequest,
- ) -> str | None:
- try:
- return self._snapshot_provider.revision_token(
- self.revision_key_for_document_id(request.document_id)
- )
- except Exception:
- return None
-
- def _current_snapshot_for_apply_result(self) -> UiSnapshotRef | None:
- try:
- return self._snapshot_provider.current_snapshot()
- except Exception:
- return None
-
-
-@dataclass(frozen=True, slots=True)
-class UiCodeDocumentProviderIdentity(UiCodeDocumentIdentity, UiWidgetIdentity):
- """Stable identity and derived names for one UI code document."""
-
- title: str
-
- @classmethod
- def from_declaration(
- cls,
- declaration: type[UiCodeDocumentIdentityDeclaration],
- *,
- title: str,
- ) -> Self:
- return cls(
- document_id=declaration.require_value(),
- widget_id=declaration.widget_id(),
- title=title,
- )
-
- @property
- def revision_key(self) -> str:
- return f"ui-code-document:{self.document_id}"
-
- @property
- def default_edit_label(self) -> str:
- return f"edit {self.document_id} via MCP"
-
-
-@dataclass(frozen=True, slots=True)
-class UiStateSurfaceProviderIdentity(UiStateSurfaceIdentity, UiWidgetIdentity):
- """Stable identity and derived names for one UI state surface."""
-
- title: str
-
- @classmethod
- def from_owner(
- cls,
- declaration: UiOwnedStateSurfaceDeclaration,
- *,
- widget_declaration: type[UiWidgetIdentityDeclaration],
- ) -> Self:
- """Project provider identity from the widget-owned declaration."""
- return cls(
- surface_id=declaration.surface_id,
- widget_id=widget_declaration.require_value(),
- title=declaration.title,
- )
-
- @property
- def revision_key(self) -> str:
- return f"ui-state-surface:{self.surface_id}"
-
-
-@dataclass(frozen=True, slots=True)
-class UiActionProviderIdentity(UiActionIdentity):
- """Stable identity and derived names for one UI action provider."""
-
- title: str
-
- @classmethod
- def from_widget_declaration(
- cls,
- declaration: type[UiWidgetIdentityDeclaration],
- *,
- title: str,
- action_id: str | None = None,
- ) -> Self:
- return cls(
- widget_id=declaration.require_value(),
- action_id=(
- declaration.action_provider_id() if action_id is None else action_id
- ),
- title=title,
- )
-
- @property
- def revision_key(self) -> str:
- return f"ui-action:{self.widget_id}:{self.action_id}"
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeProviderIdentity:
- """Stable identity shared by dynamic UI bridge provider catalogs."""
-
- provider_id: str
- title: str
-
-
-@dataclass(frozen=True, slots=True)
-class UiWindowProviderIdentity(UiBridgeProviderIdentity):
- """Stable identity for one dynamic UI window catalog provider."""
-
- @property
- def revision_key(self) -> str:
- return f"ui-window-provider:{self.provider_id}"
-
-
-@dataclass(frozen=True, slots=True)
-class UiObjectStateScopeProviderIdentity(UiBridgeProviderIdentity):
- """Stable identity for one ObjectState scope catalog provider."""
-
- @property
- def revision_key(self) -> str:
- return f"ui-object-state-provider:{self.provider_id}"
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeGuardRule:
- """One fail-loud precondition for a mutating bridge request."""
-
- code: str
- message: str
- predicate: Callable[[], bool]
-
- def error_if_violated(self) -> AgentError | None:
- if self.predicate():
- return AgentError(code=self.code, message=self.message)
- return None
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeGuardTemplate:
- """Declared guard identity that binds to a runtime predicate."""
-
- code: str
- message: str
-
- def bind(self, predicate: Callable[[], bool]) -> UiBridgeGuardRule:
- return UiBridgeGuardRule(
- code=self.code,
- message=self.message,
- predicate=predicate,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeGuardPolicy:
- """Ordered guard authority for bridge mutation preconditions."""
-
- rules: tuple[UiBridgeGuardRule, ...]
-
- def first_error(self) -> AgentError | None:
- for rule in self.rules:
- error = rule.error_if_violated()
- if error is not None:
- return error
- return None
-
-
-CONFIRMATION_REQUIRED_GUARD = UiBridgeGuardTemplate(
- code="confirmation_required",
- message="UI confirmation is required.",
-)
-RESTORE_TIME_TRAVEL_OPT_IN_GUARD = UiBridgeGuardTemplate(
- code="time_travel_branch_switch_requires_confirmation",
- message="Restoring while time-traveled requires allow_auto_branch=True.",
-)
-APPLY_TIME_TRAVEL_OPT_IN_GUARD = UiBridgeGuardTemplate(
- code="stale_time_travel_state",
- message="Applying while time-traveled requires apply_if_time_traveling=True.",
-)
-STALE_CODE_DOCUMENT_REVISION_ERROR = AgentError(
- code="stale_revision_token",
- message="The UI document changed after it was read.",
- hint=(
- "Read the document again with openhcs_ui_get_code_document and retry "
- "with its current_revision_token."
- ),
-)
diff --git a/openhcs/pyqt_gui/services/ui_bridge_live_overview.py b/openhcs/pyqt_gui/services/ui_bridge_live_overview.py
index 99813cdcd1..98f5b17ec0 100644
--- a/openhcs/pyqt_gui/services/ui_bridge_live_overview.py
+++ b/openhcs/pyqt_gui/services/ui_bridge_live_overview.py
@@ -1,45 +1,31 @@
-"""Live UI overview state-surface provider."""
+"""OpenHCS contributions to the UI live overview: viewer sessions and the overview surface."""
from __future__ import annotations
-import hashlib
-from dataclasses import replace
-
from zmqruntime.viewer_state import ViewerInstance, ViewerStateManager
-
-from openhcs.core.selection import AllItems
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiLiveOverviewItem,
UiLiveOverviewMetric,
UiLiveOverviewSection,
UiLiveOverviewSeverity,
- UiLiveOverviewState,
- UiStateSurfaceDocument,
- UiStateSurfaceRequest,
- UiStateSurfaceSummary,
-)
-from python_introspect import to_jsonable
-from openhcs.agent.ui_bridge_identities import (
- UiLiveOverviewStateSurfaceIdentityDeclaration,
)
from openhcs.core.streaming_config_declarations import ViewerFamily
-from objectstate.object_state import ObjectStateRegistry
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- UiBridgeSnapshotProviderABC,
+from formbridge.bridge.contracts import (
UiLiveOverviewContributorABC,
UiLiveOverviewContributorIdentity,
+)
+from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
+from formbridge.bridge.identities import UiLiveOverviewStateSurfaceIdentityDeclaration
+from formbridge.bridge.contracts import (
UiOwnedStateSurfaceDeclaration,
- UiStateSurfaceProviderABC,
UiStateSurfaceProviderIdentity,
state_surface_declaration_for_identity,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
+from formbridge.bridge.registry import (
UiBridgeProviderSetABC,
UiBridgeRegistrationContext,
- UiBridgeSurfaceRegistry,
)
-from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
+from formbridge.bridge.live_overview import UiLiveOverviewStateSurfaceProvider
class ViewerSessionLiveOverviewContributor(UiLiveOverviewContributorABC):
@@ -159,154 +145,3 @@ def register(self, context: UiBridgeRegistrationContext) -> None:
identity=self._identity,
)
)
-
-
-class UiLiveOverviewStateSurfaceProvider(UiStateSurfaceProviderABC):
- """Pollable UI overview assembled from registered provider contributions."""
-
- def __init__(
- self,
- *,
- registry: UiBridgeSurfaceRegistry,
- snapshot_provider: UiBridgeSnapshotProviderABC,
- declaration: UiOwnedStateSurfaceDeclaration,
- identity: UiStateSurfaceProviderIdentity,
- ) -> None:
- self._registry = registry
- self._snapshot_provider = snapshot_provider
- self._declaration = declaration
- self.identity = identity
-
- def summary(self) -> UiStateSurfaceSummary:
- return UiStateSurfaceSummary(
- schema_version=SCHEMA_VERSION,
- identity=self.identity.as_surface_identity(),
- title=self.identity.title,
- widget_id=self.identity.widget_id,
- readable=True,
- supported_selection_modes=(AllItems.name,),
- current_selection_count=0,
- total_scope_count=1,
- )
-
- def read(self, request: UiStateSurfaceRequest) -> UiStateSurfaceDocument:
- selection_mode = request.resolved_selection_mode(
- AllItems
- )
- try:
- state = UiLiveOverviewState(
- schema_version=SCHEMA_VERSION,
- summary=self.summary(),
- object_state_token=ObjectStateRegistry.get_token(),
- sections=self._sections(),
- selected_scope_ids=(),
- current_revision_token=self._snapshot_provider.revision_token(
- self.identity.revision_key
- ),
- current_snapshot=self._snapshot_provider.current_snapshot(),
- )
- except Exception as exc:
- return self._state_error(
- request,
- (AgentError.from_exception("ui_state_surface_read_failed", exc),),
- )
- revision_token = self._revision_token(state, selection_mode=selection_mode)
- state = replace(
- state,
- current_revision_token=revision_token,
- current_snapshot=self._snapshot_provider.current_snapshot(),
- unchanged=request.base_revision_token == revision_token,
- )
- return self._document_from_state(state, selection_mode=selection_mode)
-
- def _sections(self) -> tuple[UiLiveOverviewSection, ...]:
- sections: list[UiLiveOverviewSection] = []
- for provider in self._registry.overview_contributors():
- try:
- sections.extend(provider.overview_sections())
- except Exception as exc:
- sections.append(
- self._provider_error_section(provider.overview_identity, exc)
- )
- return tuple(sections)
-
- @staticmethod
- def _provider_error_section(
- identity: UiLiveOverviewContributorIdentity,
- exc: Exception,
- ) -> UiLiveOverviewSection:
- error = AgentError.from_exception("ui_live_overview_provider_failed", exc)
- return UiLiveOverviewSection(
- section_id=identity.section_id,
- title=identity.title,
- summary="overview unavailable",
- items=(
- UiLiveOverviewItem(
- label=error.code,
- status=error.exception_type,
- detail=error.message,
- severity=UiLiveOverviewSeverity.ERROR.value,
- ),
- ),
- )
-
- def _revision_token(
- self,
- state: UiLiveOverviewState,
- *,
- selection_mode: str,
- ) -> str:
- parts = (
- self.identity.revision_key,
- str(state.object_state_token),
- self._snapshot_provider.current_branch_head_snapshot_id(),
- str(ObjectStateRegistry.get_current_snapshot_index()),
- selection_mode,
- state.sections,
- )
- return hashlib.sha256(repr(parts).encode("utf-8")).hexdigest()
-
- def _state_error(
- self,
- request: UiStateSurfaceRequest,
- errors: tuple[AgentError, ...],
- ) -> UiStateSurfaceDocument:
- selection_mode = request.resolved_selection_mode(
- AllItems
- )
- state = UiLiveOverviewState(
- schema_version=SCHEMA_VERSION,
- summary=self.summary(),
- object_state_token=ObjectStateRegistry.get_token(),
- sections=(),
- selected_scope_ids=(),
- current_revision_token=self._snapshot_provider.revision_token(
- self.identity.revision_key
- ),
- current_snapshot=self._snapshot_provider.current_snapshot(),
- errors=errors,
- )
- return self._document_from_state(state, selection_mode=selection_mode)
-
- def _document_from_state(
- self,
- state: UiLiveOverviewState,
- *,
- selection_mode: str,
- ) -> UiStateSurfaceDocument:
- payload = to_jsonable(state)
- if not isinstance(payload, dict):
- raise TypeError("Live overview payload did not serialize to an object.")
- return UiStateSurfaceDocument(
- schema_version=state.schema_version,
- summary=state.summary,
- payload_schema=self._declaration.payload_schema,
- payload=payload,
- current_revision_token=state.current_revision_token,
- current_snapshot=state.current_snapshot,
- selection_mode=selection_mode,
- selected_scope_ids=state.selected_scope_ids,
- unchanged=state.unchanged,
- warnings=state.warnings,
- errors=state.errors,
- )
diff --git a/openhcs/pyqt_gui/services/ui_bridge_object_state.py b/openhcs/pyqt_gui/services/ui_bridge_object_state.py
deleted file mode 100644
index 178d982182..0000000000
--- a/openhcs/pyqt_gui/services/ui_bridge_object_state.py
+++ /dev/null
@@ -1,1163 +0,0 @@
-"""ObjectState scope projection provider for the PyQt UI bridge."""
-
-from __future__ import annotations
-
-import hashlib
-import logging
-from collections.abc import Callable, Mapping, Sequence, Set
-from dataclasses import dataclass
-from dataclasses import is_dataclass
-from enum import Enum
-from typing import ClassVar
-
-from pyqt_reactive.services.scope_window_navigation import ScopeWindowNavigationService
-from pyqt_reactive.services.function_pattern_code_document import (
- FunctionPatternScopeCodeDocumentDriver,
-)
-from pyqt_reactive.forms.parameter_form_service import ParameterFormService
-from pyqt_reactive.services.window_code_document import (
- PYTHON_MIME_TYPE,
-)
-from pyqt_reactive.services.window_manager import WindowManager
-from pyqt_reactive.services.window_navigation import WindowNavigationRequest
-from python_introspect import (
- JsonValue,
- UnifiedParameterAnalyzer,
- project_dataclass,
- to_jsonable,
-)
-
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
- AllFields,
- UiCatalogPageMetadata,
- UiCodeDocument,
- UiCodeDocumentApplyRequest,
- UiCodeDocumentApplyResult,
- UiCodeDocumentRequest,
- UiCodeDocumentSummary,
- UiCodeDocumentValidationRequest,
- UiCodeDocumentValidationResult,
- UiMutationReceipt,
- UiObjectStateFieldMutationRequest,
- UiObjectStateFieldMutationResult,
- UiObjectStateFieldProvenance,
- UiObjectStateFieldSemanticCarrier,
- UiObjectStateFieldSummary,
- UiObjectStateScopeCatalog,
- UiObjectStateScopeIdentity,
- UiObjectStateScopeListRequest,
- UiObjectStateScopeSummary,
- UiObjectStateValuePreview,
- UiSemanticAddress,
-)
-from objectstate import ObjectState, ObjectStateRegistry
-from objectstate.inspection import FieldInspection, type_name, type_qualname
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- APPLY_TIME_TRAVEL_OPT_IN_GUARD,
- CONFIRMATION_REQUIRED_GUARD,
- STALE_CODE_DOCUMENT_REVISION_ERROR,
- UiBridgeGuardPolicy,
- UiBridgeSnapshotProviderABC,
- UiCodeDocumentProviderIdentity,
- UiObjectStateScopeProviderABC,
- UiObjectStateScopeProviderIdentity,
- SnapshotBackedUiCodeDocumentProviderABC,
-)
-from openhcs.pyqt_gui.services.ui_agent_bridge import UiCodeDocumentApplyLabel
-from openhcs.pyqt_gui.services.ui_bridge_object_state_scope_policy import (
- ObjectStateScopeVisibility,
-)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
- UiBridgeProviderSetABC,
- UiBridgeRegistrationContext,
-)
-from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
-
-OBJECT_STATE_SCOPE_PROVIDER_ID = "object_state.scopes"
-OBJECT_STATE_SCOPE_CODE_DOCUMENT_ID = "object_state_scope"
-OBJECT_STATE_SCOPE_CODE_DOCUMENT_PREFIX = f"{OBJECT_STATE_SCOPE_CODE_DOCUMENT_ID}:"
-WINDOW_CODE_DOCUMENT_ID = "window_code_document"
-WINDOW_CODE_DOCUMENT_PREFIX = f"{WINDOW_CODE_DOCUMENT_ID}:"
-OBJECT_STATE_FIELD_PAGE_LIMIT_MAX = 1000
-OBJECT_STATE_FIELD_VALUE_REPR_LIMIT = 500
-OBJECT_STATE_FIELD_VALUE_PREVIEW_LIMIT = 160
-OBJECT_STATE_FIELD_VALUE_PREVIEW_ITEMS = 4
-
-logger = logging.getLogger(__name__)
-
-
-def agent_object_state_scope_id(scope_id: str | None) -> str | None:
- """Return the stable agent-facing id for an ObjectState registry scope."""
-
- if scope_id is None:
- return None
- return OpenHCSUiWindowId.agent_window_id_for_manager_scope(scope_id)
-
-
-@dataclass(frozen=True, slots=True)
-class ObjectStateScopeCodeDocumentAddress:
- """Nominal code-document address for one ObjectState scope."""
-
- scope_id: str
-
- prefix: ClassVar[str] = OBJECT_STATE_SCOPE_CODE_DOCUMENT_PREFIX
- minimum_scope_id_length: ClassVar[int] = 1
-
- @classmethod
- def parse(cls, document_id: str) -> "ObjectStateScopeCodeDocumentAddress":
- if not document_id.startswith(cls.prefix):
- raise ValueError(
- "ObjectState scope code document ids must start with "
- f"{cls.prefix!r}; got {document_id!r}."
- )
- scope_id = document_id[len(cls.prefix) :]
- if len(scope_id) < cls.minimum_scope_id_length:
- raise ValueError(
- "ObjectState scope code document id is missing a scope id."
- )
- return cls(scope_id=scope_id)
-
- @property
- def document_id(self) -> str:
- return f"{self.prefix}{self.scope_id}"
-
- @property
- def revision_key(self) -> str:
- return f"ui-code-document:{self.document_id}"
-
- def provider_identity(self, title: str) -> UiCodeDocumentProviderIdentity:
- return UiCodeDocumentProviderIdentity(
- document_id=self.document_id,
- widget_id=OBJECT_STATE_SCOPE_CODE_DOCUMENT_ID,
- title=title,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class WindowCodeDocumentAddress:
- """Nominal code-document address for one open WindowManager code mode."""
-
- window_id: str
-
- prefix: ClassVar[str] = WINDOW_CODE_DOCUMENT_PREFIX
-
- @classmethod
- def parse(cls, document_id: str) -> "WindowCodeDocumentAddress":
- if not document_id.startswith(cls.prefix):
- raise ValueError(
- "Window code-document ids must start with "
- f"{cls.prefix!r}; got {document_id!r}."
- )
- window_id = document_id[len(cls.prefix) :]
- if not window_id:
- raise ValueError("Window code-document id is missing a window id.")
- return cls(window_id=window_id)
-
- @classmethod
- def from_scope_id(cls, scope_id: str) -> "WindowCodeDocumentAddress":
- return cls(
- window_id=OpenHCSUiWindowId.agent_window_id_for_manager_scope(scope_id)
- )
-
- @property
- def document_id(self) -> str:
- return f"{self.prefix}{self.window_id}"
-
- @property
- def revision_key(self) -> str:
- return f"ui-code-document:{self.document_id}"
-
- def provider_identity(self, title: str) -> UiCodeDocumentProviderIdentity:
- return UiCodeDocumentProviderIdentity(
- document_id=self.document_id,
- widget_id=self.window_id,
- title=title,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class ObjectStateFieldSemanticProjection(FieldInspection):
- """One inspected ObjectState field projected onto the bridge's DTOs."""
-
- @property
- def raw_json_value(self) -> JsonValue:
- return self.json_value(self.raw_value)
-
- @property
- def resolved_json_value(self) -> JsonValue | None:
- if not self.resolved_value_available:
- return None
- return self.json_value(self.resolved_value)
-
- @property
- def raw_value_preview(self) -> UiObjectStateValuePreview:
- return self.value_preview(self.raw_value)
-
- @property
- def resolved_value_preview(self) -> UiObjectStateValuePreview | None:
- if not self.resolved_value_available:
- return None
- return self.value_preview(self.resolved_value)
-
- def semantic_address(self, *, window_id: str | None = None) -> UiSemanticAddress:
- return UiSemanticAddress(
- object_state_scope_id=self.agent_scope_id,
- field_path=self.field_path,
- window_id=window_id,
- )
-
- @property
- def agent_scope_id(self) -> str:
- return agent_object_state_scope_id(self.state.scope_id) or self.state.scope_id
-
- def to_semantic_carrier(
- self,
- *,
- include_values: bool = False,
- ) -> UiObjectStateFieldSemanticCarrier:
- """Project the shared DTO semantics owned by this live field."""
-
- return UiObjectStateFieldSemanticCarrier(
- dirty=self.dirty,
- signature_diff=self.signature_diff,
- last_changed=self.last_changed,
- semantic_markers=self.semantic_markers,
- raw_value=self.raw_json_value if include_values else None,
- resolved_value=self.resolved_json_value if include_values else None,
- raw_value_preview=self.raw_value_preview,
- resolved_value_preview=self.resolved_value_preview,
- raw_value_is_none=self.raw_value_is_none,
- resolved_value_is_none=self.resolved_value_is_none,
- inherited_value=self.inherited_value,
- provenance=self.provenance_dto,
- )
-
- def to_field_summary(
- self,
- *,
- include_values: bool = False,
- include_description: bool = False,
- ) -> UiObjectStateFieldSummary:
- parameter_description = None
- if include_description:
- parameter_description = self.state.parameter_descriptions.get(
- self.field_path
- )
- return project_dataclass(
- UiObjectStateFieldSummary,
- self.to_semantic_carrier(include_values=include_values),
- schema_version=SCHEMA_VERSION,
- address=self.semantic_address(
- window_id=OpenHCSUiWindowId.agent_window_id_for_manager_scope(
- self.state.scope_id
- ),
- ),
- field_name=self.field_name,
- container_path=self.container_path,
- object_state_path_type=self.declaring_type,
- raw_value_type=self.raw_value_type,
- resolved_value_type=self.resolved_value_type,
- parameter_description=parameter_description,
- )
-
- @property
- def provenance_dto(self) -> UiObjectStateFieldProvenance | None:
- if self.provenance is None:
- return None
- return UiObjectStateFieldProvenance(
- source_scope_id=agent_object_state_scope_id(self.provenance.source_scope_id),
- source_type=type_qualname(self.provenance.source_type),
- source_field_path=self.provenance.source_field_path,
- )
-
- @staticmethod
- def json_value(value) -> JsonValue:
- try:
- return to_jsonable(value)
- except TypeError:
- value_repr = repr(value)
- if len(value_repr) > OBJECT_STATE_FIELD_VALUE_REPR_LIMIT:
- value_repr = (
- f"{value_repr[:OBJECT_STATE_FIELD_VALUE_REPR_LIMIT]}..."
- )
- return value_repr
-
- @classmethod
- def value_preview(cls, value) -> UiObjectStateValuePreview:
- text, truncated = cls.preview_text(value)
- return UiObjectStateValuePreview(
- type_name=type_name(value),
- is_none=value is None,
- text=text,
- truncated=truncated,
- )
-
- @classmethod
- def preview_text(cls, value) -> tuple[str, bool]:
- text = cls._preview_text_unbounded(value)
- truncated = len(text) > OBJECT_STATE_FIELD_VALUE_PREVIEW_LIMIT
- if truncated:
- text = f"{text[:OBJECT_STATE_FIELD_VALUE_PREVIEW_LIMIT]}..."
- return text, truncated
-
- @classmethod
- def _preview_text_unbounded(cls, value) -> str:
- if value is None:
- return "None"
- if callable(value):
- return cls._callable_preview_text(value)
- if isinstance(value, Enum):
- return f"{type(value).__name__}.{value.name}"
- if isinstance(value, str):
- return repr(value)
- if isinstance(value, bytes):
- return f"bytes(len={len(value)})"
- if isinstance(value, Mapping):
- keys = tuple(
- cls._preview_item_text(key) for key in cls._first_items(value.keys())
- )
- return f"{type(value).__name__}(len={len(value)}, keys={keys!r})"
- if isinstance(value, (Sequence, Set)) and not isinstance(
- value,
- (str, bytes, bytearray),
- ):
- items = tuple(
- cls._preview_item_text(item) for item in cls._first_items(value)
- )
- return f"{type(value).__name__}(len={len(value)}, items={items!r})"
- if is_dataclass(value) and not isinstance(value, type):
- return repr(value)
- return repr(value)
-
- @staticmethod
- def _first_items(values) -> tuple[object, ...]:
- items = []
- for item in values:
- items.append(item)
- if len(items) >= OBJECT_STATE_FIELD_VALUE_PREVIEW_ITEMS:
- break
- return tuple(items)
-
- @staticmethod
- def _preview_item_text(value) -> str:
- if callable(value):
- return ObjectStateFieldSemanticProjection._callable_preview_text(value)
- if isinstance(value, Enum):
- return f"{type(value).__name__}.{value.name}"
- return repr(value)
-
- @staticmethod
- def _callable_preview_text(value: Callable[..., object]) -> str:
- try:
- payload = to_jsonable(value)
- except TypeError:
- return repr(value)
- if not isinstance(payload, Mapping):
- return repr(value)
- import_path = payload.get("import_path")
- if not isinstance(import_path, str):
- return repr(value)
- return import_path
-
-
-class ObjectStateScopeProjectionService:
- """Project ObjectState registry entries into bounded agent DTOs."""
-
- def catalog(
- self,
- request: UiObjectStateScopeListRequest,
- ) -> UiObjectStateScopeCatalog:
- visibility = ObjectStateScopeVisibility(request)
- scopes = tuple(
- self.summary(state, request)
- for state in ObjectStateRegistry.get_all()
- if visibility.includes_scope_id(state.scope_id)
- )
- return UiObjectStateScopeCatalog(
- schema_version=SCHEMA_VERSION,
- object_state_token=ObjectStateRegistry.get_token(),
- current_branch=ObjectStateRegistry.get_current_branch(),
- current_snapshot_index=ObjectStateRegistry.get_current_snapshot_index(),
- active=ObjectStateRegistry.is_time_traveling(),
- scopes=scopes,
- )
-
- def summary(
- self,
- state: ObjectState,
- request: UiObjectStateScopeListRequest,
- ) -> UiObjectStateScopeSummary:
- fields, field_page = self._field_page(state, request)
- return UiObjectStateScopeSummary(
- schema_version=SCHEMA_VERSION,
- identity=UiObjectStateScopeIdentity(
- object_state_scope_id=agent_object_state_scope_id(state.scope_id)
- or state.scope_id,
- ),
- object_type=type(state.object_instance).__name__,
- parameter_count=len(state.parameters),
- dirty_field_count=len(state.dirty_fields),
- signature_diff_field_count=len(state.signature_diff_fields),
- has_unsaved_changes=bool(state.dirty_fields),
- has_default_overrides=bool(state.signature_diff_fields),
- last_changed_field=state.last_changed_field,
- fields=fields,
- field_page=field_page,
- )
-
- def _field_page(
- self,
- state: ObjectState,
- request: UiObjectStateScopeListRequest,
- ) -> tuple[tuple[UiObjectStateFieldSummary, ...], UiCatalogPageMetadata | None]:
- if not request.include_fields:
- return (), None
-
- if request.field_paths:
- all_field_paths = tuple(
- field_path
- for field_path in request.field_paths
- if field_path in state.parameters
- )
- else:
- all_field_paths = tuple(sorted(state.parameters.keys()))
- if request.field_filter is not AllFields:
- all_field_paths = tuple(
- field_path
- for field_path in all_field_paths
- if request.field_filter.matches(
- self._field_summary(
- state,
- field_path,
- include_values=False,
- include_description=False,
- ),
- )
- )
- offset = max(0, request.field_offset)
- limit = min(max(0, request.field_limit), OBJECT_STATE_FIELD_PAGE_LIMIT_MAX)
- selected_paths = all_field_paths[offset : offset + limit]
- next_offset = offset + len(selected_paths)
- truncated = next_offset < len(all_field_paths)
- next_page_offset = None
- if truncated:
- next_page_offset = next_offset
- page = UiCatalogPageMetadata(
- limit=limit,
- offset=offset,
- returned_count=len(selected_paths),
- total_count=len(all_field_paths),
- truncated=truncated,
- next_offset=next_page_offset,
- )
- fields = tuple(
- self._field_summary(
- state,
- field_path,
- include_values=request.include_field_values,
- include_description=request.include_field_descriptions,
- )
- for field_path in selected_paths
- )
- return fields, page
-
- def _field_summary(
- self,
- state: ObjectState,
- field_path: str,
- *,
- include_values: bool,
- include_description: bool,
- ) -> UiObjectStateFieldSummary:
- return ObjectStateFieldSemanticProjection.of(
- state,
- field_path,
- ).to_field_summary(
- include_values=include_values,
- include_description=include_description,
- )
-
-
-def object_state_for_agent_scope_id(scope_id: str) -> ObjectState | None:
- """The ObjectState an agent-facing scope or window id names."""
- for candidate_scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- scope_id
- ):
- state = ObjectStateRegistry.get_by_scope(candidate_scope_id)
- if state is not None:
- return state
- return None
-
-
-class ObjectStateFieldMutationService:
- """Mutate one ObjectState field through ObjectState's own update/reset API."""
-
- def __init__(
- self,
- before_mutation: Callable[[str], None] | None = None,
- ) -> None:
- self._before_mutation = before_mutation
-
- def mutate(
- self,
- request: UiObjectStateFieldMutationRequest,
- ) -> UiObjectStateFieldMutationResult:
- state = object_state_for_agent_scope_id(
- request.object_state_scope_id
- )
- if state is None:
- return self._error_result(
- request,
- AgentError(
- code="ui_object_state_scope_not_found",
- message=(
- "ObjectState scope is not registered in the running UI: "
- f"{request.object_state_scope_id!r}"
- ),
- ),
- )
- if request.field_path not in state.parameters:
- return self._error_result(
- request,
- AgentError(
- code="ui_object_state_field_not_found",
- message=(
- "ObjectState field is not registered on this scope: "
- f"{request.field_path!r}"
- ),
- ),
- )
-
- before = self._field_summary(state, request)
- try:
- if self._before_mutation is not None:
- self._before_mutation(request.object_state_scope_id)
- if request.reset:
- state.reset_parameter(request.field_path)
- else:
- state.update_parameter(
- request.field_path,
- self._converted_value(state, request),
- )
- after = self._field_summary(state, request)
- return UiObjectStateFieldMutationResult(
- schema_version=SCHEMA_VERSION,
- address=after.address,
- mutated=True,
- reset=request.reset,
- receipt=UiMutationReceipt.accepted_for(request.request_token),
- before=before,
- after=after,
- )
- except Exception as exc:
- return self._error_result(
- request,
- AgentError.from_exception("ui_object_state_field_mutation_failed", exc),
- before=before,
- )
-
- @staticmethod
- def _field_summary(
- state: ObjectState,
- request: UiObjectStateFieldMutationRequest,
- ) -> UiObjectStateFieldSummary:
- return ObjectStateFieldSemanticProjection.of(
- state,
- request.field_path,
- ).to_field_summary(include_values=request.include_field_values)
-
- @staticmethod
- def _converted_value(
- state: ObjectState,
- request: UiObjectStateFieldMutationRequest,
- ) -> object:
- help_target = state.type_for_path(request.field_path)
- parameter_name = request.field_path.rsplit(".", 1)[-1]
- parameter_info = UnifiedParameterAnalyzer.analyze(help_target).get(
- parameter_name
- )
- if parameter_info is None:
- return request.value
- return ParameterFormService().convert_value_to_type(
- request.value,
- parameter_info.param_type,
- parameter_name,
- help_target,
- )
-
- @staticmethod
- def _error_result(
- request: UiObjectStateFieldMutationRequest,
- error: AgentError,
- *,
- before: UiObjectStateFieldSummary | None = None,
- ) -> UiObjectStateFieldMutationResult:
- return UiObjectStateFieldMutationResult(
- schema_version=SCHEMA_VERSION,
- address=UiSemanticAddress(
- object_state_scope_id=request.object_state_scope_id,
- field_path=request.field_path,
- window_id=request.window_id,
- ),
- mutated=False,
- reset=request.reset,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- before=before,
- errors=(error,),
- )
-
-
-class ObjectStateScopeProvider(UiObjectStateScopeProviderABC):
- """ObjectState scope catalog provider."""
-
- identity = UiObjectStateScopeProviderIdentity(
- provider_id=OBJECT_STATE_SCOPE_PROVIDER_ID,
- title="ObjectState scopes",
- )
-
- def __init__(self) -> None:
- self._projection = ObjectStateScopeProjectionService()
-
- def catalog(
- self,
- request: UiObjectStateScopeListRequest,
- ) -> UiObjectStateScopeCatalog:
- return self._projection.catalog(request)
-
-
-class WindowCodeDocumentDriverBackedProvider(SnapshotBackedUiCodeDocumentProviderABC):
- """Shared provider flow for WindowCodeDocumentDriver-backed documents."""
-
- def __init__(self, snapshot_provider: UiBridgeSnapshotProviderABC) -> None:
- self._snapshot_provider = snapshot_provider
-
- def read(self, request: UiCodeDocumentRequest) -> UiCodeDocument:
- try:
- address = self._address(request.document_id)
- driver = self._driver(address)
- document = driver.read_document(clean=request.clean)
- source_bytes = document.source.encode("utf-8")
- summary = self._summary_for_address(address, document.title)
- return UiCodeDocument(
- schema_version=SCHEMA_VERSION,
- summary=summary,
- source=document.source,
- mime_type=document.mime_type,
- size_bytes=len(source_bytes),
- sha256=hashlib.sha256(source_bytes).hexdigest(),
- current_revision_token=self._current_revision_token(address),
- current_snapshot=self._snapshot_provider.current_snapshot(),
- selection_mode=request.selection_mode,
- selected_scope_ids=self._selected_scope_ids(address),
- )
- except Exception as exc:
- return self._document_error(
- request,
- AgentError.from_exception("ui_code_document_read_failed", exc),
- )
-
- def validate(
- self,
- request: UiCodeDocumentValidationRequest,
- ) -> UiCodeDocumentValidationResult:
- try:
- address = self._address(request.document_id)
- self._driver(address).validate_source(request.source)
- return UiCodeDocumentValidationResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- valid=True,
- normalized_scope_ids=self._selected_scope_ids(address),
- )
- except Exception as exc:
- return UiCodeDocumentValidationResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- valid=False,
- errors=(
- AgentError.from_exception(
- "ui_code_document_validation_failed", exc
- ),
- ),
- )
-
- def apply(self, request: UiCodeDocumentApplyRequest) -> UiCodeDocumentApplyResult:
- guard_error = self._apply_guard_policy(request).first_error()
- if guard_error is not None:
- return self._apply_error(request, guard_error)
-
- try:
- address = self._address(request.document_id)
- current_revision = self._current_revision_token(address)
- if request.base_revision_token != current_revision:
- return self._apply_error(
- request,
- STALE_CODE_DOCUMENT_REVISION_ERROR,
- )
-
- driver = self._driver(address)
- if not driver.writable():
- return self._apply_error(
- request,
- AgentError(
- code="ui_code_document_read_only",
- message="The UI code document is currently read-only.",
- ),
- )
- current_document = driver.read_document(clean=True)
- current_source = current_document.source
- if driver.source_is_current(
- request.source,
- current_document=current_document,
- ):
- return self._apply_unchanged(request)
- driver.validate_source(request.source)
- ObjectStateRegistry.ensure_baseline_snapshot()
- pre_snapshot = self._snapshot_provider.current_snapshot()
- pre_head_id = self._snapshot_provider.current_branch_head_snapshot_id()
- label = UiCodeDocumentApplyLabel.resolve(
- request,
- self._provider_identity_for_label(address),
- ).value
- with ObjectStateRegistry.atomic_success(
- label,
- self._mutation_scope_id(address),
- ):
- self._before_apply(address)
- driver.apply_source(request.source)
-
- post_head_id = self._snapshot_provider.current_branch_head_snapshot_id()
- if (
- driver.records_object_state_snapshot_on_apply()
- and post_head_id == pre_head_id
- ):
- if driver.read_document(clean=True).source == current_source:
- return self._apply_unchanged(request)
- return self._apply_error(
- request,
- AgentError(
- code="snapshot_not_recorded",
- message=(
- "Applying the UI document did not record a new "
- "ObjectState snapshot."
- ),
- ),
- )
-
- post_snapshot = self._snapshot_provider.current_snapshot()
- new_revision_token = self._current_revision_token(address)
- return UiCodeDocumentApplyResult(
- schema_version=SCHEMA_VERSION,
- document_id=request.document_id,
- applied=True,
- base_revision_token=request.base_revision_token,
- receipt=UiMutationReceipt.accepted_for(request.request_token),
- outcome="applied",
- new_revision_token=new_revision_token,
- current_revision_token=new_revision_token,
- current_snapshot=post_snapshot,
- undo_snapshot=pre_snapshot,
- pre_apply_snapshot=pre_snapshot,
- post_apply_snapshot=post_snapshot,
- )
- except Exception as exc:
- logger.exception(
- "Failed to apply UI code document %s",
- request.document_id,
- )
- return self._apply_error(
- request,
- AgentError.from_exception("ui_code_document_apply_failed", exc),
- )
-
- @staticmethod
- def _apply_guard_policy(
- request: UiCodeDocumentApplyRequest,
- ) -> UiBridgeGuardPolicy:
- return UiBridgeGuardPolicy(
- rules=(
- CONFIRMATION_REQUIRED_GUARD.bind(
- lambda: request.confirmation_is_required(),
- ),
- APPLY_TIME_TRAVEL_OPT_IN_GUARD.bind(
- lambda: (
- ObjectStateRegistry.is_time_traveling()
- and not request.apply_if_time_traveling
- ),
- ),
- )
- )
-
- def _address(self, document_id: str):
- raise NotImplementedError
-
- def _before_apply(self, address) -> None:
- del address
-
- def _driver(self, address):
- raise NotImplementedError
-
- def _summary_for_address(self, address, title: str) -> UiCodeDocumentSummary:
- raise NotImplementedError
-
- def _selected_scope_ids(self, address) -> tuple[str, ...]:
- raise NotImplementedError
-
- def _revision_key_for_address(self, address) -> str:
- raise NotImplementedError
-
- def _provider_identity_for_label(self, address) -> UiCodeDocumentProviderIdentity:
- raise NotImplementedError
-
- def _mutation_scope_id(self, address) -> str:
- raise NotImplementedError
-
- def _current_revision_token_for_apply_result(
- self,
- request: UiCodeDocumentApplyRequest,
- ) -> str | None:
- try:
- address = self._address(request.document_id)
- return self._current_revision_token(address)
- except Exception:
- return super()._current_revision_token_for_apply_result(request)
-
- def _current_revision_token(
- self,
- address,
- ) -> str:
- return self._snapshot_provider.revision_token(
- self._revision_key_for_address(address)
- )
-
- def _document_error(
- self,
- request: UiCodeDocumentRequest,
- error: AgentError,
- ) -> UiCodeDocument:
- source_bytes = b""
- return UiCodeDocument(
- schema_version=SCHEMA_VERSION,
- summary=self.summary(),
- source="",
- mime_type=PYTHON_MIME_TYPE,
- size_bytes=0,
- sha256=hashlib.sha256(source_bytes).hexdigest(),
- current_revision_token=self._snapshot_provider.revision_token(
- self.identity.revision_key
- ),
- current_snapshot=self._snapshot_provider.current_snapshot(),
- selection_mode=request.selection_mode,
- selected_scope_ids=(),
- errors=(error,),
- )
-
-
-class ObjectStateScopeCodeDocumentProvider(WindowCodeDocumentDriverBackedProvider):
- """Dynamic code-document provider for WindowManager-backed ObjectState scopes."""
-
- identity = UiCodeDocumentProviderIdentity(
- document_id=OBJECT_STATE_SCOPE_CODE_DOCUMENT_ID,
- widget_id=OBJECT_STATE_SCOPE_CODE_DOCUMENT_ID,
- title="ObjectState scope code documents",
- )
-
- def __init__(
- self,
- snapshot_provider: UiBridgeSnapshotProviderABC,
- before_mutation: Callable[[str], None] | None = None,
- ) -> None:
- super().__init__(snapshot_provider)
- self._before_mutation = before_mutation
-
- def _before_apply(self, address: ObjectStateScopeCodeDocumentAddress) -> None:
- if self._before_mutation is not None:
- self._before_mutation(self._scope_id(address))
-
- def handles(self, document_id: str) -> bool:
- return document_id == self.identity.document_id or document_id.startswith(
- OBJECT_STATE_SCOPE_CODE_DOCUMENT_PREFIX
- )
-
- def summaries(self) -> tuple[UiCodeDocumentSummary, ...]:
- summaries = tuple(
- self._summary_for_scope_id(scope_id)
- for scope_id in self._readable_scope_ids()
- )
- if not summaries:
- return (self.summary(),)
- return summaries
-
- def revision_key_for_document_id(self, document_id: str) -> str:
- return self._revision_key_for_address(self._address(document_id))
-
- def summary(self) -> UiCodeDocumentSummary:
- return UiCodeDocumentSummary(
- schema_version=SCHEMA_VERSION,
- identity=self.identity.as_document_identity(),
- widget_id=self.identity.widget_id,
- title=(
- "ObjectState scope code documents "
- f"({OBJECT_STATE_SCOPE_CODE_DOCUMENT_PREFIX})"
- ),
- readable=False,
- writable=False,
- total_scope_count=len(ObjectStateRegistry.get_all()),
- )
-
- def _readable_scope_ids(self) -> tuple[str, ...]:
- return tuple(
- agent_object_state_scope_id(state.scope_id) or state.scope_id
- for state in ObjectStateRegistry.get_all()
- if self._has_code_document_driver(state.scope_id)
- )
-
- def _has_code_document_driver(self, scope_id: str) -> bool:
- if FunctionPatternScopeCodeDocumentDriver.handles_scope(scope_id):
- return True
- code_document_scopes = WindowManager.get_code_document_scopes()
- return any(
- candidate_scope_id in code_document_scopes
- for candidate_scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- agent_object_state_scope_id(scope_id) or scope_id
- )
- )
-
- def _summary_for_scope_id(self, scope_id: str) -> UiCodeDocumentSummary:
- address = ObjectStateScopeCodeDocumentAddress(scope_id=scope_id)
- title = self._title_for_scope_id(scope_id)
- return self._summary_for_address(address, title)
-
- def _title_for_scope_id(self, scope_id: str) -> str:
- for candidate_scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- scope_id
- ):
- window = WindowManager.get_window(candidate_scope_id)
- if window is not None:
- return f"Code mode - {window.windowTitle()}"
- return f"Code mode - {scope_id}"
-
- def _address(self, document_id: str) -> ObjectStateScopeCodeDocumentAddress:
- return ObjectStateScopeCodeDocumentAddress.parse(document_id)
-
- def _driver(self, address: ObjectStateScopeCodeDocumentAddress):
- scope_id = self._scope_id(address)
- if FunctionPatternScopeCodeDocumentDriver.handles_scope(scope_id):
- return FunctionPatternScopeCodeDocumentDriver(scope_id)
- return WindowManager.require_code_document_driver(scope_id)
-
- def _scope_id(self, address: ObjectStateScopeCodeDocumentAddress) -> str:
- for scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- address.scope_id
- ):
- if FunctionPatternScopeCodeDocumentDriver.handles_scope(scope_id):
- return scope_id
- try:
- WindowManager.require_code_document_driver(scope_id)
- return scope_id
- except KeyError:
- continue
- for scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- address.scope_id
- ):
- result = ScopeWindowNavigationService.navigate(
- WindowNavigationRequest(
- scope_id=scope_id,
- create_if_missing=True,
- )
- )
- if result.window is not None:
- return scope_id
- raise KeyError(
- f"ObjectState scope window is not available: {address.scope_id!r}"
- )
-
- def _summary_for_address(
- self,
- address: ObjectStateScopeCodeDocumentAddress,
- title: str,
- ) -> UiCodeDocumentSummary:
- identity = address.provider_identity(title)
- writable = self._driver(address).writable()
- return UiCodeDocumentSummary(
- schema_version=SCHEMA_VERSION,
- identity=identity.as_document_identity(),
- widget_id=identity.widget_id,
- title=title,
- readable=True,
- writable=writable,
- current_selection_count=1,
- total_scope_count=len(ObjectStateRegistry.get_all()),
- )
-
- def _selected_scope_ids(
- self,
- address: ObjectStateScopeCodeDocumentAddress,
- ) -> tuple[str, ...]:
- return (address.scope_id,)
-
- def _revision_key_for_address(
- self,
- address: ObjectStateScopeCodeDocumentAddress,
- ) -> str:
- return address.revision_key
-
- def _provider_identity_for_label(
- self,
- address: ObjectStateScopeCodeDocumentAddress,
- ) -> UiCodeDocumentProviderIdentity:
- return address.provider_identity(self.identity.title)
-
- def _mutation_scope_id(self, address: ObjectStateScopeCodeDocumentAddress) -> str:
- return self._scope_id(address)
-
-
-class WindowManagerCodeDocumentProvider(WindowCodeDocumentDriverBackedProvider):
- """Discover and route live WindowManager code-mode documents."""
-
- identity = UiCodeDocumentProviderIdentity(
- document_id=WINDOW_CODE_DOCUMENT_ID,
- widget_id=WINDOW_CODE_DOCUMENT_ID,
- title="Open window code-mode documents",
- )
-
- def handles(self, document_id: str) -> bool:
- return document_id.startswith(WINDOW_CODE_DOCUMENT_PREFIX)
-
- def revision_key_for_document_id(self, document_id: str) -> str:
- return self._revision_key_for_address(self._address(document_id))
-
- def summary(self) -> UiCodeDocumentSummary:
- return UiCodeDocumentSummary(
- schema_version=SCHEMA_VERSION,
- identity=self.identity.as_document_identity(),
- widget_id=self.identity.widget_id,
- title=(
- "Open window code-mode documents "
- f"({WINDOW_CODE_DOCUMENT_PREFIX})"
- ),
- readable=False,
- writable=False,
- total_scope_count=len(WindowManager.get_code_document_scopes()),
- )
-
- def summaries(self) -> tuple[UiCodeDocumentSummary, ...]:
- scopes = WindowManager.get_code_document_scopes()
- if not scopes:
- return (self.summary(),)
- return tuple(self._summary_for_open_scope(scope_id) for scope_id in scopes)
-
- def _summary_for_open_scope(self, scope_id: str) -> UiCodeDocumentSummary:
- address = WindowCodeDocumentAddress.from_scope_id(scope_id)
- window = WindowManager.get_window(scope_id)
- title = self._open_scope_title(scope_id, address, window)
- return self._summary_for_address(address, title)
-
- def _open_scope_title(
- self,
- scope_id: str,
- address: WindowCodeDocumentAddress,
- window,
- ) -> str:
- if window is not None and window.windowTitle():
- return f"Code mode - {window.windowTitle()}"
- try:
- document_title = (
- WindowManager.require_code_document_driver(scope_id)
- .read_document(clean=True)
- .title
- )
- except Exception:
- document_title = address.window_id
- return f"Code mode - {document_title}"
-
- def _address(self, document_id: str) -> WindowCodeDocumentAddress:
- return WindowCodeDocumentAddress.parse(document_id)
-
- def _driver(self, address: WindowCodeDocumentAddress):
- return WindowManager.require_code_document_driver(self._scope_id(address))
-
- def _scope_id(self, address: WindowCodeDocumentAddress) -> str:
- for scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- address.window_id
- ):
- try:
- WindowManager.require_code_document_driver(scope_id)
- return scope_id
- except KeyError:
- continue
- raise KeyError(
- f"Open window has no code-document driver registered: {address.window_id!r}"
- )
-
- def _summary_for_address(
- self,
- address: WindowCodeDocumentAddress,
- title: str,
- ) -> UiCodeDocumentSummary:
- identity = address.provider_identity(title)
- writable = self._driver(address).writable()
- return UiCodeDocumentSummary(
- schema_version=SCHEMA_VERSION,
- identity=identity.as_document_identity(),
- widget_id=identity.widget_id,
- title=title,
- readable=True,
- writable=writable,
- current_selection_count=1,
- total_scope_count=len(WindowManager.get_code_document_scopes()),
- )
-
- def _selected_scope_ids(
- self,
- address: WindowCodeDocumentAddress,
- ) -> tuple[str, ...]:
- return (self._scope_id(address),)
-
- def _revision_key_for_address(self, address: WindowCodeDocumentAddress) -> str:
- return address.revision_key
-
- def _current_revision_token(self, address: WindowCodeDocumentAddress) -> str:
- return self._driver(address).current_revision_token()
-
- def _provider_identity_for_label(
- self,
- address: WindowCodeDocumentAddress,
- ) -> UiCodeDocumentProviderIdentity:
- return address.provider_identity(self.identity.title)
-
- def _mutation_scope_id(self, address: WindowCodeDocumentAddress) -> str:
- return self._scope_id(address)
-
-
-class ObjectStateBridgeProviderSet(UiBridgeProviderSetABC):
- """Provider set for ObjectState registry projections."""
-
- registry_key = OBJECT_STATE_SCOPE_PROVIDER_ID
-
- def __init__(
- self,
- before_mutation: Callable[[str], None] | None = None,
- ) -> None:
- self._before_mutation = before_mutation
-
- @classmethod
- def for_main_window(cls, main_window) -> "ObjectStateBridgeProviderSet":
- return cls(
- main_window.session.require_definition_mutation_allowed_for_object_scope
- )
-
- def register(self, context: UiBridgeRegistrationContext) -> None:
- context.registry.register_object_state_scope_provider(
- ObjectStateScopeProvider()
- )
- context.registry.register_code_document_provider(
- ObjectStateScopeCodeDocumentProvider(
- context.snapshot_provider,
- before_mutation=self._before_mutation,
- )
- )
- context.registry.register_code_document_provider(
- WindowManagerCodeDocumentProvider(context.snapshot_provider)
- )
diff --git a/openhcs/pyqt_gui/services/ui_bridge_object_state_scope_policy.py b/openhcs/pyqt_gui/services/ui_bridge_object_state_scope_policy.py
deleted file mode 100644
index 760a0a1e17..0000000000
--- a/openhcs/pyqt_gui/services/ui_bridge_object_state_scope_policy.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""ObjectState scope visibility policy shared by UI bridge projections."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass, field
-from typing import ClassVar
-
-from openhcs.agent.dto.ui_bridge import UiObjectStateScopeVisibility
-from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
-
-
-@dataclass(frozen=True, slots=True)
-class ObjectStateScopeVisibility:
- """Agent-facing visibility policy for ObjectState scope ids."""
-
- hidden_unaliased_system_scopes: ClassVar[frozenset[str]] = frozenset(
- ("__plates__",)
- )
-
- request: UiObjectStateScopeVisibility = field(
- default_factory=UiObjectStateScopeVisibility
- )
-
- def includes_system_scopes(self) -> bool:
- return self.request.include_system_scopes
-
- def includes_scope_id(self, scope_id: str) -> bool:
- if self.includes_system_scopes() or self.has_agent_alias(scope_id):
- return True
- return scope_id not in self.hidden_unaliased_system_scopes
-
- @staticmethod
- def has_agent_alias(scope_id: str) -> bool:
- return OpenHCSUiWindowId.agent_window_id_for_manager_scope(scope_id) != scope_id
diff --git a/openhcs/pyqt_gui/services/ui_bridge_pipeline_editor.py b/openhcs/pyqt_gui/services/ui_bridge_pipeline_editor.py
index 9377036cf2..f3a2bd61df 100644
--- a/openhcs/pyqt_gui/services/ui_bridge_pipeline_editor.py
+++ b/openhcs/pyqt_gui/services/ui_bridge_pipeline_editor.py
@@ -5,38 +5,41 @@
import hashlib
from dataclasses import replace
-from openhcs.core.selection import AllItems, SelectionScope
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.selection import AllItems, SelectionScope
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.bridge.dto import (
UiActionIdentity,
UiActionInvokeRequest,
UiActionSummary,
- UiDebugActionState,
- UiDebugCursorState,
- UiDebugRuntimeFrameState,
- UiDebugTerminalSummaryState,
UiLiveOverviewItem,
UiLiveOverviewMetric,
UiLiveOverviewSection,
UiLiveOverviewSeverity,
- UiPipelineDebugSessionState,
- UiPipelineEditorState,
- UiProgressIdentityState,
UiStateSurfaceDocument,
UiStateSurfaceRequest,
UiStateSurfaceSummary,
)
+from openhcs.agent.dto.ui_bridge import (
+ UiDebugActionState,
+ UiDebugCursorState,
+ UiDebugRuntimeFrameState,
+ UiDebugTerminalSummaryState,
+ UiPipelineDebugSessionState,
+ UiPipelineEditorState,
+ UiProgressIdentityState,
+)
from openhcs.agent.ui_bridge_identities import (
PipelineDebugSessionStateSurfaceIdentityDeclaration,
PipelineDebugToolbarWidgetIdentity,
PipelineEditorStateSurfaceIdentityDeclaration,
PipelineEditorWidgetIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
+ DatasetListStateSurfaceIdentity,
)
from python_introspect import to_jsonable
from objectstate.object_state import ObjectStateRegistry
from openhcs.core.progress.debug_projection import DebugRuntimeFrame
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
+from formbridge.bridge.contracts import (
UiActionDispatch,
UiActionProviderABC,
UiActionProviderIdentity,
@@ -46,10 +49,7 @@
state_surface_declaration_for_identity,
state_surface_ids_for_action,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
- UiBridgeProviderSetABC,
- UiBridgeRegistrationContext,
-)
+from formbridge.bridge.registry import UiBridgeProviderSetABC, UiBridgeRegistrationContext
from openhcs.authoring.session.views import PipelineStepsView
from openhcs.pyqt_gui.services.ui_bridge_session_actions import (
SessionOperationActionProvider,
@@ -64,7 +64,7 @@
PIPELINE_EDITOR_ACTIONS_TITLE = "Pipeline editor actions"
PIPELINE_DEBUG_TOOLBAR_ACTIONS_TITLE = "Pipeline debug toolbar actions"
PLATE_MANAGER_STATE_SURFACE_ID = (
- PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ DatasetListStateSurfaceIdentity.require_value()
)
PIPELINE_EDITOR_WIDGET_ID = PipelineEditorWidgetIdentity.require_value()
PIPELINE_DEBUG_TOOLBAR_WIDGET_ID = PipelineDebugToolbarWidgetIdentity.require_value()
diff --git a/openhcs/pyqt_gui/services/ui_bridge_plate_manager.py b/openhcs/pyqt_gui/services/ui_bridge_plate_manager.py
index 54a7b937ca..d61aa85702 100644
--- a/openhcs/pyqt_gui/services/ui_bridge_plate_manager.py
+++ b/openhcs/pyqt_gui/services/ui_bridge_plate_manager.py
@@ -5,9 +5,10 @@
import hashlib
from dataclasses import replace
-from openhcs.core.selection import AllItems, SelectedItems, SelectionScope
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.selection import AllItems, SelectedItems, SelectionScope
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.bridge.dto import (
UiCodeDocument,
UiCodeDocumentApplyRequest,
UiCodeDocumentApplyResult,
@@ -15,9 +16,6 @@
UiCodeDocumentSummary,
UiCodeDocumentValidationRequest,
UiCodeDocumentValidationResult,
- UiLiveMeasurementEntryState,
- UiLiveMeasurementsState,
- UiPlateManagerState,
UiLiveOverviewItem,
UiLiveOverviewMetric,
UiLiveOverviewSection,
@@ -27,11 +25,16 @@
UiStateSurfaceSummary,
UiMutationReceipt,
)
+from openhcs.agent.dto.ui_bridge import (
+ UiLiveMeasurementEntryState,
+ UiLiveMeasurementsState,
+ UiPlateManagerState,
+)
from openhcs.agent.ui_bridge_identities import (
- PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration,
- PlateManagerOrchestratorCodeDocumentIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
- PlateManagerWidgetIdentity,
+ DatasetLiveMeasurementsStateSurfaceIdentity,
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+ DatasetListWidgetIdentity,
)
from python_introspect import to_jsonable
from objectstate.object_state import ObjectStateRegistry
@@ -42,13 +45,12 @@
)
from openhcs.authoring.session.views import DatasetListView
from pyqt_reactive.services.scope_color_service import ScopeColorService
+from formbridge.bridge.in_process import UiCodeDocumentApplyLabel, UiCodeDocumentValidationError
from openhcs.pyqt_gui.services.ui_agent_bridge import (
- UiCodeDocumentApplyLabel,
UiCodeDocumentExecutionService,
UiCodeDocumentSourcePolicy,
- UiCodeDocumentValidationError,
)
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
+from formbridge.bridge.contracts import (
APPLY_TIME_TRAVEL_OPT_IN_GUARD,
CONFIRMATION_REQUIRED_GUARD,
STALE_CODE_DOCUMENT_REVISION_ERROR,
@@ -63,10 +65,7 @@
state_surface_declaration_for_identity,
state_surface_ids_for_action,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
- UiBridgeProviderSetABC,
- UiBridgeRegistrationContext,
-)
+from formbridge.bridge.registry import UiBridgeProviderSetABC, UiBridgeRegistrationContext
from openhcs.pyqt_gui.services.ui_bridge_session_actions import (
SessionOperationActionProvider,
)
@@ -80,13 +79,13 @@
PYTHON_MIME_TYPE = "text/x-python"
PLATE_MANAGER_ORCHESTRATOR_IDENTITY = UiCodeDocumentProviderIdentity.from_declaration(
- PlateManagerOrchestratorCodeDocumentIdentity,
+ DatasetListCodeDocumentIdentity,
title=ORCHESTRATOR_DOCUMENT_TITLE,
)
PLATE_MANAGER_STATE_DECLARATION: UiOwnedStateSurfaceDeclaration = (
state_surface_declaration_for_identity(
PlateManagerWidget.UI_STATE_SURFACE_DECLARATIONS,
- PlateManagerStateSurfaceIdentityDeclaration,
+ DatasetListStateSurfaceIdentity,
)
)
PLATE_MANAGER_STATE_IDENTITY = UiStateSurfaceProviderIdentity.from_owner(
@@ -96,7 +95,7 @@
LIVE_MEASUREMENTS_STATE_DECLARATION: UiOwnedStateSurfaceDeclaration = (
state_surface_declaration_for_identity(
PlateManagerWidget.UI_STATE_SURFACE_DECLARATIONS,
- PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration,
+ DatasetLiveMeasurementsStateSurfaceIdentity,
)
)
LIVE_MEASUREMENTS_STATE_IDENTITY = UiStateSurfaceProviderIdentity.from_owner(
@@ -105,20 +104,20 @@
)
PLATE_MANAGER_ACTION_PROVIDER_IDENTITY = (
UiActionProviderIdentity.from_widget_declaration(
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
title=PLATE_MANAGER_ACTIONS_TITLE,
)
)
PLATE_MANAGER_STATE_SURFACE_ID = PLATE_MANAGER_STATE_DECLARATION.surface_id
PLATE_MANAGER_ACTION_STATE_SURFACES = (PLATE_MANAGER_STATE_SURFACE_ID,)
PLATE_MANAGER_CODE_DOCUMENT_ID = (
- PlateManagerOrchestratorCodeDocumentIdentity.require_value()
+ DatasetListCodeDocumentIdentity.require_value()
)
class PlateManagerBridgeProviderSet(UiBridgeProviderSetABC):
"""Register all PlateManager surfaces with a UI bridge registry."""
- registry_key = PlateManagerWidgetIdentity.require_value()
+ registry_key = DatasetListWidgetIdentity.require_value()
def __init__(self, manager, *, execution_service=None) -> None:
self._manager = manager
@@ -529,7 +528,7 @@ def _overview_row_item(cls, row: DatasetRowState) -> UiLiveOverviewItem:
detail=cls._overview_row_detail(row),
severity=cls._overview_row_severity(row).value,
source_surface_id=PLATE_MANAGER_STATE_SURFACE_ID,
- source_widget_id=PlateManagerWidgetIdentity.require_value(),
+ source_widget_id=DatasetListWidgetIdentity.require_value(),
)
@staticmethod
diff --git a/openhcs/pyqt_gui/services/ui_bridge_registry.py b/openhcs/pyqt_gui/services/ui_bridge_registry.py
deleted file mode 100644
index 8f4feca7bb..0000000000
--- a/openhcs/pyqt_gui/services/ui_bridge_registry.py
+++ /dev/null
@@ -1,263 +0,0 @@
-"""Provider registry for the PyQt UI bridge surface."""
-
-from __future__ import annotations
-
-from abc import ABC, abstractmethod
-from dataclasses import dataclass
-from enum import Enum
-from typing import ClassVar, TypeAlias, TypeVar
-
-from metaclass_registry import AutoRegisterMeta, LazyDiscoveryDict, RegistryConfig
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- UiBridgeRegistryKeyMixin,
- UiLiveOverviewContributorABC,
- UiBridgeSnapshotProviderABC,
- UiActionProviderABC,
- UiCodeDocumentProviderABC,
- UiObjectStateScopeProviderABC,
- UiStateSurfaceProviderABC,
- UiWindowProviderABC,
-)
-
-UiBridgeProvider: TypeAlias = (
- UiCodeDocumentProviderABC
- | UiStateSurfaceProviderABC
- | UiActionProviderABC
- | UiWindowProviderABC
- | UiObjectStateScopeProviderABC
-)
-DynamicProviderT = TypeVar(
- "DynamicProviderT",
- UiCodeDocumentProviderABC,
- UiWindowProviderABC,
-)
-
-
-class UiRegisteredSurfaceKind(str, Enum):
- """Kinds of UI surfaces currently exposed through the bridge registry."""
-
- CODE_DOCUMENT = "code_document"
- STATE_SURFACE = "state_surface"
- ACTION = "action"
- WINDOW = "window"
- OBJECT_STATE_SCOPE = "object_state_scope"
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeRegistrationContext:
- """Context passed to provider sets during bridge composition."""
-
- registry: "UiBridgeSurfaceRegistry"
- snapshot_provider: UiBridgeSnapshotProviderABC
-
-
-_UI_BRIDGE_PROVIDER_SET_REGISTRY = LazyDiscoveryDict(enable_cache=False)
-
-
-class UiBridgeProviderSetABC(
- UiBridgeRegistryKeyMixin,
- ABC,
- metaclass=AutoRegisterMeta,
- registry_config=RegistryConfig(
- registry_dict=_UI_BRIDGE_PROVIDER_SET_REGISTRY,
- key_attribute="registry_key",
- skip_if_no_key=True,
- registry_name="PyQt UI bridge provider set",
- discovery_package="openhcs.pyqt_gui.services",
- ),
-):
- """Composable collection of providers for one widget/domain."""
-
- __registry__ = _UI_BRIDGE_PROVIDER_SET_REGISTRY
- compose_for_main_window: ClassVar[bool] = True
-
- @classmethod
- @abstractmethod
- def for_main_window(cls, main_window) -> "UiBridgeProviderSetABC":
- """Construct this declaration's provider set for the owning main window."""
- raise NotImplementedError
-
- @abstractmethod
- def register(self, context: UiBridgeRegistrationContext) -> None:
- raise NotImplementedError
-
-
-class CompositeUiBridgeProviderSet(UiBridgeProviderSetABC):
- """Register several provider sets through one composition stage."""
-
- registry_key = "composite"
- compose_for_main_window = False
-
- def __init__(self, provider_sets: tuple[UiBridgeProviderSetABC, ...]) -> None:
- self._provider_sets = provider_sets
-
- @classmethod
- def for_main_window(cls, main_window) -> "UiBridgeProviderSetABC":
- del main_window
- raise TypeError(
- "Composite provider sets are assembled by the composition root."
- )
-
- def register(self, context: UiBridgeRegistrationContext) -> None:
- for provider_set in self._provider_sets:
- provider_set.register(context)
-
-
-class UiBridgeSurfaceRegistry:
- """Nominal registry for UI bridge provider discovery and routing."""
-
- def __init__(self) -> None:
- self._code_document_providers: dict[str, UiCodeDocumentProviderABC] = {}
- self._state_surface_providers: dict[str, UiStateSurfaceProviderABC] = {}
- self._action_providers: dict[str, UiActionProviderABC] = {}
- self._window_providers: dict[str, UiWindowProviderABC] = {}
- self._object_state_scope_providers: dict[str, UiObjectStateScopeProviderABC] = (
- {}
- )
- self._live_overview_contributors: list[UiLiveOverviewContributorABC] = []
-
- def register_code_document_provider(
- self,
- provider: UiCodeDocumentProviderABC,
- ) -> None:
- document_id = provider.identity.document_id
- self._register_unique(
- self._code_document_providers,
- document_id,
- provider,
- UiRegisteredSurfaceKind.CODE_DOCUMENT,
- )
-
- def register_state_surface_provider(
- self,
- provider: UiStateSurfaceProviderABC,
- ) -> None:
- surface_id = provider.identity.surface_id
- self._register_unique(
- self._state_surface_providers,
- surface_id,
- provider,
- UiRegisteredSurfaceKind.STATE_SURFACE,
- )
-
- def register_action_provider(
- self,
- provider: UiActionProviderABC,
- ) -> None:
- provider_id = provider.identity.widget_id
- self._register_unique(
- self._action_providers,
- provider_id,
- provider,
- UiRegisteredSurfaceKind.ACTION,
- )
-
- def register_window_provider(
- self,
- provider: UiWindowProviderABC,
- ) -> None:
- provider_id = provider.identity.provider_id
- self._register_unique(
- self._window_providers,
- provider_id,
- provider,
- UiRegisteredSurfaceKind.WINDOW,
- )
-
- def register_object_state_scope_provider(
- self,
- provider: UiObjectStateScopeProviderABC,
- ) -> None:
- provider_id = provider.identity.provider_id
- self._register_unique(
- self._object_state_scope_providers,
- provider_id,
- provider,
- UiRegisteredSurfaceKind.OBJECT_STATE_SCOPE,
- )
-
- def register_live_overview_contributor(
- self,
- contributor: UiLiveOverviewContributorABC,
- ) -> None:
- self._live_overview_contributors.append(contributor)
-
- def code_document_provider(self, document_id: str) -> UiCodeDocumentProviderABC:
- if document_id in self._code_document_providers:
- return self._code_document_providers[document_id]
- return self._dynamic_provider(
- self._code_document_providers,
- document_id,
- unknown_message=f"Unknown UI code document: {document_id}",
- )
-
- def state_surface_provider(self, surface_id: str) -> UiStateSurfaceProviderABC:
- try:
- return self._state_surface_providers[surface_id]
- except KeyError as exc:
- raise KeyError(f"Unknown UI state surface: {surface_id}") from exc
-
- def action_provider(self, widget_id: str) -> UiActionProviderABC:
- try:
- return self._action_providers[widget_id]
- except KeyError as exc:
- raise KeyError(f"Unknown UI action provider: {widget_id}") from exc
-
- def window_provider(self, window_id: str) -> UiWindowProviderABC:
- return self._dynamic_provider(
- self._window_providers,
- window_id,
- unknown_message=f"Unknown UI window: {window_id}",
- )
-
- def code_document_providers(self) -> tuple[UiCodeDocumentProviderABC, ...]:
- return tuple(self._code_document_providers.values())
-
- def state_surface_providers(self) -> tuple[UiStateSurfaceProviderABC, ...]:
- return tuple(self._state_surface_providers.values())
-
- def action_providers(self) -> tuple[UiActionProviderABC, ...]:
- return tuple(self._action_providers.values())
-
- def window_providers(self) -> tuple[UiWindowProviderABC, ...]:
- return tuple(self._window_providers.values())
-
- def object_state_scope_providers(
- self,
- ) -> tuple[UiObjectStateScopeProviderABC, ...]:
- return tuple(self._object_state_scope_providers.values())
-
- def overview_contributors(self) -> tuple[UiLiveOverviewContributorABC, ...]:
- return (
- tuple(self._state_surface_providers.values())
- + tuple(self._window_providers.values())
- + tuple(self._live_overview_contributors)
- )
-
- @staticmethod
- def _dynamic_provider(
- providers: dict[str, DynamicProviderT],
- surface_id: str,
- *,
- unknown_message: str,
- ) -> DynamicProviderT:
- for provider in providers.values():
- if provider.handles(surface_id):
- return provider
- raise KeyError(unknown_message)
-
- @staticmethod
- def _register_unique(
- providers: dict[
- str,
- UiBridgeProvider,
- ],
- provider_id: str,
- provider: UiBridgeProvider,
- surface_kind: UiRegisteredSurfaceKind,
- ) -> None:
- if provider_id in providers:
- raise ValueError(
- f"Duplicate UI bridge {surface_kind.value} provider id: {provider_id}"
- )
- providers[provider_id] = provider
diff --git a/openhcs/pyqt_gui/services/ui_bridge_server.py b/openhcs/pyqt_gui/services/ui_bridge_server.py
deleted file mode 100644
index b57e350fdd..0000000000
--- a/openhcs/pyqt_gui/services/ui_bridge_server.py
+++ /dev/null
@@ -1,623 +0,0 @@
-"""Process boundary server for the OpenHCS UI agent bridge."""
-
-from __future__ import annotations
-
-import json
-import os
-import pickle
-import threading
-import time
-from collections.abc import Callable
-from dataclasses import dataclass
-from pathlib import Path
-from typing import ClassVar
-
-from metaclass_registry import AutoRegisterMeta
-from python_introspect import (
- JsonObject,
- dataclass_from_mapping,
- project_dataclass,
- to_jsonable,
-)
-from zmqruntime.messages import (
- ControlErrorResponse,
- ControlMessageType,
- ControlRequestHeader,
- PongResponse,
- ServerRole,
-)
-from zmqruntime.timeouts import OperationDeadline
-from zmqruntime.transport import (
- TransportEndpoint,
- endpoint_startup_lock,
- resolve_transport_mode,
-)
-
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError
-from openhcs.agent.dto.execution import ExecutionConnectionSpec
-from openhcs.agent.dto.ui_bridge import (
- UiBridgeConnectionSpec,
- UiBridgeDescriptorFile,
- UiBridgeDescriptorWirePayload,
- UiBridgeRequestEnvelope,
- UiBridgeResponseEnvelope,
-)
-from openhcs.agent.runtime_platform import AgentRuntimePlatform
-from openhcs.agent.services.ui_bridge_service import (
- UI_BRIDGE_PROTOCOL_VERSION,
- UiBridgeOperation,
-)
-from openhcs.pyqt_gui.config import AgentUiBridgeConfig
-from openhcs.pyqt_gui.services.ui_agent_bridge import UiAgentBridgeService
-from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
-from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG, OpenHCSZMQConfig
-
-DEFAULT_UI_BRIDGE_START_TIMEOUT_SECONDS = 5.0
-UI_BRIDGE_BROWSER_SERVER_NAME = "OpenHCSUiBridgeServer"
-
-
-@dataclass(frozen=True, slots=True)
-class UiBridgeServerBinding(UiBridgeConnectionSpec):
- def __post_init__(self) -> None:
- missing_fields = tuple(
- field_name
- for field_name, value in (
- ("port", self.port),
- ("descriptor_file_path", self.descriptor_file_path),
- ("bridge_instance_id", self.bridge_instance_id),
- ("auth_token", self.auth_token),
- )
- if value is None
- )
- if missing_fields:
- raise ValueError(
- "UI bridge binding requires resolved fields: "
- + ", ".join(missing_fields)
- )
-
- @classmethod
- def from_runtime(
- cls,
- *,
- connection: ExecutionConnectionSpec,
- bridge_instance_id: str,
- descriptor_file_path: Path,
- auth_token: str,
- ) -> "UiBridgeServerBinding":
- if connection.port is None:
- raise ValueError("UI bridge binding requires a resolved data port.")
- return project_dataclass(
- cls,
- connection,
- auth_token=auth_token,
- descriptor_file_path=str(descriptor_file_path),
- bridge_instance_id=bridge_instance_id,
- )
-
- @property
- def connection(self) -> ExecutionConnectionSpec:
- return self.public_connection()
-
- def token_bearing_connection(self) -> UiBridgeConnectionSpec:
- return self
-
-
-@dataclass(slots=True)
-class UiBridgeUnsupportedOperationError(LookupError):
- """Raised when an agent requests an operation this bridge does not expose."""
-
- operation_name: str
-
- def __str__(self) -> str:
- return f"Unsupported UI bridge operation: {self.operation_name}"
-
-
-@dataclass(slots=True)
-class UiBridgeEndpointInUseError(RuntimeError):
- """Raised before a UI bridge can replace another live endpoint pair."""
-
- endpoint: TransportEndpoint
- occupied_ports: frozenset[int]
-
- def __str__(self) -> str:
- ports = ", ".join(str(port) for port in sorted(self.occupied_ports))
- return (
- f"UI bridge endpoint {self.endpoint.transport_mode.value}://"
- f"{self.endpoint.host}:{self.endpoint.port} is already in use "
- f"(occupied ports: {ports})."
- )
-
-
-class UiBridgeRequestExceptionClassifier(metaclass=AutoRegisterMeta):
- """Registered classifier for request-dispatch exception codes."""
-
- __registry_key__ = "exception_key"
- __skip_if_no_key__ = True
-
- exception_type: ClassVar[type[BaseException] | None] = None
- exception_key: ClassVar[str | None] = None
- error_code: ClassVar[str]
-
- @classmethod
- def __init_subclass__(cls, **kwargs):
- super().__init_subclass__(**kwargs)
- if cls.exception_type is not None and cls.exception_key is None:
- cls.exception_key = cls.exception_type_key(cls.exception_type)
-
- @staticmethod
- def exception_type_key(exception_type: type[BaseException]) -> str:
- return f"{exception_type.__module__}.{exception_type.__qualname__}"
-
- @classmethod
- def classifier_for_exception(
- cls,
- exception: Exception,
- ) -> type["UiBridgeRequestExceptionClassifier"] | None:
- for exception_type in type(exception).__mro__:
- classifier = cls.__registry__.get(cls.exception_type_key(exception_type))
- if classifier is not None:
- return classifier
- return None
-
-
-class UiBridgePermissionErrorClassifier(UiBridgeRequestExceptionClassifier):
- exception_type = PermissionError
- error_code = "ui_bridge_auth_failed"
-
-
-class UiBridgeUnsupportedOperationErrorClassifier(UiBridgeRequestExceptionClassifier):
- exception_type = UiBridgeUnsupportedOperationError
- error_code = "unsupported_ui_bridge_operation"
-
-
-class UiBridgeValueErrorClassifier(UiBridgeRequestExceptionClassifier):
- exception_type = ValueError
- error_code = "invalid_ui_bridge_request"
-
-
-class UiBridgeRequestErrorAuthority:
- """Classify request-dispatch exceptions into agent-facing error codes."""
-
- DEFAULT_ERROR_CODE = "ui_bridge_request_failed"
-
- @classmethod
- def agent_error(cls, exception: Exception) -> AgentError:
- classifier = UiBridgeRequestExceptionClassifier.classifier_for_exception(
- exception
- )
- if classifier is not None:
- return AgentError.from_exception(classifier.error_code, exception)
- return AgentError.from_exception(cls.DEFAULT_ERROR_CODE, exception)
-
-
-class UiBridgeRequestDispatcher:
- """Route validated UI bridge envelopes to the in-process bridge service."""
-
- def __init__(
- self,
- bridge: UiAgentBridgeService,
- *,
- auth_token: str,
- binding_supplier: Callable[[], UiBridgeServerBinding],
- ) -> None:
- self._bridge = bridge
- self._auth_token = auth_token
- self._binding_supplier = binding_supplier
-
- @property
- def bridge(self) -> UiAgentBridgeService:
- return self._bridge
-
- def dispatch(self, payload: JsonObject) -> JsonObject:
- try:
- request = dataclass_from_mapping(
- UiBridgeRequestEnvelope,
- payload,
- )
- self._validate_request_contract(request)
- operation = self._operation(request.operation)
- self._validate_auth(request, operation)
- result = operation.as_served(
- self._bridge.invoke(
- operation, operation.decode_request(request.payload)
- ),
- self._binding_supplier(),
- )
- response = UiBridgeResponseEnvelope(
- schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
- application=OPENHCS_ENDPOINT_APPLICATION,
- request_id=request.request_id,
- ok=True,
- payload=self._result_payload(result),
- )
- except Exception as exc:
- response = self._error_response(payload, exc)
- return self._response_payload(response)
-
- def _validate_request_contract(self, request: UiBridgeRequestEnvelope) -> None:
- if request.schema_version != SCHEMA_VERSION:
- raise ValueError(
- f"Unsupported agent schema version: {request.schema_version}"
- )
- if request.bridge_protocol_version != UI_BRIDGE_PROTOCOL_VERSION:
- raise ValueError(
- f"Unsupported UI bridge protocol version: {request.bridge_protocol_version}"
- )
- OPENHCS_ENDPOINT_APPLICATION.compatibility_with(
- request.application
- ).require_match()
-
- @staticmethod
- def _operation(operation_name: str) -> type[UiBridgeOperation]:
- try:
- return UiBridgeOperation.for_name(operation_name)
- except KeyError as exc:
- raise UiBridgeUnsupportedOperationError(operation_name) from exc
-
- def _validate_auth(
- self,
- request: UiBridgeRequestEnvelope,
- operation: type[UiBridgeOperation],
- ) -> None:
- if operation.requires_auth and request.auth_token != self._auth_token:
- raise PermissionError("UI bridge auth token is missing or invalid.")
-
- @staticmethod
- def _result_payload(result) -> JsonObject:
- payload = to_jsonable(result)
- if not isinstance(payload, dict):
- raise TypeError(
- f"UI bridge operation result must serialize to a JSON object, "
- f"got {type(payload).__name__}"
- )
- return payload
-
- @staticmethod
- def _response_payload(response: UiBridgeResponseEnvelope) -> JsonObject:
- payload = to_jsonable(response)
- if not isinstance(payload, dict):
- raise TypeError(
- "UI bridge response envelope did not serialize to a JSON object."
- )
- return payload
-
- def _error_response(
- self,
- payload: JsonObject,
- exception: Exception,
- ) -> UiBridgeResponseEnvelope:
- return UiBridgeResponseEnvelope(
- schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
- application=OPENHCS_ENDPOINT_APPLICATION,
- request_id=self._request_id_from_payload(payload),
- ok=False,
- payload={},
- errors=(self._agent_error(exception),),
- )
-
- @staticmethod
- def _request_id_from_payload(payload: JsonObject) -> str:
- request_id = payload.get("request_id")
- if isinstance(request_id, str) and request_id:
- return request_id
- return "unknown"
-
- @staticmethod
- def _agent_error(exception: Exception) -> AgentError:
- return UiBridgeRequestErrorAuthority.agent_error(exception)
-
-
-class UiBridgeControlServer:
- """Background ZMQ REP server exposing one UiAgentBridgeService."""
-
- def __init__(
- self,
- bridge: UiAgentBridgeService,
- config: AgentUiBridgeConfig | None = None,
- transport_config: OpenHCSZMQConfig = OPENHCS_ZMQ_CONFIG,
- ) -> None:
- self._bridge = bridge
- self._config = config or AgentUiBridgeConfig()
- self._transport_config = transport_config
- self._bridge_instance_id = self._config.resolve_bridge_instance_id()
- self._auth_token = self._config.resolve_auth_token()
- self._stop_event = threading.Event()
- self._ready_event = threading.Event()
- self._thread: threading.Thread | None = None
- self._binding: UiBridgeServerBinding | None = None
- self._startup_error: Exception | None = None
-
- @property
- def binding(self) -> UiBridgeServerBinding:
- if self._binding is None:
- raise RuntimeError("UI bridge server is not running.")
- return self._binding
-
- @property
- def config(self) -> AgentUiBridgeConfig:
- """Return the exact immutable configuration owned by this server."""
-
- return self._config
-
- @property
- def transport_config(self) -> OpenHCSZMQConfig:
- """Return the exact immutable transport configuration owned by this server."""
-
- return self._transport_config
-
- @property
- def is_running(self) -> bool:
- thread = self._thread
- return (
- not self._stop_event.is_set()
- and thread is not None
- and thread.is_alive()
- and self._binding is not None
- )
-
- def matches_configuration(
- self,
- config: AgentUiBridgeConfig,
- transport_config: OpenHCSZMQConfig,
- ) -> bool:
- """Return whether this live server owns the exact declarations."""
-
- return (
- self.is_running
- and self._config == config
- and self._transport_config == transport_config
- )
-
- def start(
- self,
- *,
- timeout_seconds: float = DEFAULT_UI_BRIDGE_START_TIMEOUT_SECONDS,
- ) -> UiBridgeServerBinding:
- if self.is_running:
- return self.binding
- if self._stop_event.is_set():
- raise RuntimeError("A stopped UI bridge server cannot be restarted.")
- deadline = OperationDeadline.after_milliseconds(
- max(1, int(timeout_seconds * 1000)),
- operation="UI bridge endpoint startup",
- )
- endpoint = self._requested_endpoint()
- if endpoint is None:
- return self._start_before_deadline(deadline)
- with endpoint_startup_lock(
- endpoint.port,
- endpoint.transport_mode,
- self._transport_config,
- operation_deadline=deadline,
- ) as acquired:
- if not acquired:
- raise TimeoutError("UI bridge endpoint startup was cancelled.")
- self._require_available_endpoint(endpoint)
- return self._start_before_deadline(deadline)
-
- def _start_before_deadline(
- self,
- deadline: OperationDeadline,
- ) -> UiBridgeServerBinding:
- self._ready_event.clear()
- self._startup_error = None
- self._thread = threading.Thread(
- target=self._serve,
- name=f"OpenHCSUiBridge-{self._bridge_instance_id}",
- daemon=True,
- )
- self._thread.start()
- if not self._ready_event.wait(deadline.remaining_seconds()):
- self.stop()
- if self._startup_error is not None:
- error = self._startup_error
- raise RuntimeError(
- f"Failed to start UI bridge server: {error}"
- ) from error
- raise deadline.timeout_error()
- if self._startup_error is not None:
- error = self._startup_error
- self.stop()
- raise RuntimeError(f"Failed to start UI bridge server: {error}") from error
- return self.binding
-
- def stop(self) -> None:
- self._stop_event.set()
- self._bridge.close()
- thread = self._thread
- if thread is not None:
- thread.join(self._config.shutdown_timeout_seconds)
- if thread.is_alive():
- raise TimeoutError("Timed out waiting for UI bridge server to stop.")
- self._remove_descriptor_file(
- Path(self._binding.descriptor_file_path)
- if self._binding is not None
- else None
- )
- self._thread = None
- self._binding = None
-
- def _serve(self) -> None:
- import zmq
-
- context = zmq.Context()
- socket = context.socket(zmq.REP)
- browser_control_socket = context.socket(zmq.REP)
- socket.setsockopt(zmq.LINGER, 0)
- browser_control_socket.setsockopt(zmq.LINGER, 0)
- descriptor_file_path: Path | None = None
- connection: ExecutionConnectionSpec | None = None
- try:
- connection = self._bind(socket)
- self._bind_browser_control_socket(browser_control_socket, connection)
- descriptor_file_path = self._config.descriptor_path_for(
- self._bridge_instance_id
- )
- self._binding = UiBridgeServerBinding.from_runtime(
- connection=connection,
- bridge_instance_id=self._bridge_instance_id,
- descriptor_file_path=descriptor_file_path,
- auth_token=self._auth_token,
- )
- self._write_descriptor_file(
- UiBridgeDescriptorFile(
- schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
- application=OPENHCS_ENDPOINT_APPLICATION,
- bridge_instance_id=self._bridge_instance_id,
- pid=os.getpid(),
- started_at_unix=time.time(),
- connection=connection,
- auth_token=self._auth_token,
- descriptor_file_path=str(descriptor_file_path),
- )
- )
- dispatcher = UiBridgeRequestDispatcher(
- self._bridge,
- auth_token=self._auth_token,
- binding_supplier=lambda: self.binding,
- )
- poller = zmq.Poller()
- poller.register(socket, zmq.POLLIN)
- poller.register(browser_control_socket, zmq.POLLIN)
- self._ready_event.set()
- while not self._stop_event.is_set():
- events = dict(poller.poll(self._config.poll_timeout_ms))
- if socket in events:
- request_payload = socket.recv_json()
- if not isinstance(request_payload, dict):
- request_payload = {}
- socket.send_json(dispatcher.dispatch(request_payload))
- if browser_control_socket in events:
- browser_control_socket.send(
- self._browser_control_response_payload(
- browser_control_socket.recv(),
- connection,
- )
- )
- except Exception as exc:
- self._startup_error = exc
- self._ready_event.set()
- finally:
- self._remove_descriptor_file(descriptor_file_path)
- browser_control_socket.close(linger=0)
- socket.close(linger=0)
- context.term()
- if connection is not None:
- connection.transport_endpoint().cleanup(self._transport_config)
-
- def _requested_endpoint(self) -> TransportEndpoint | None:
- requested_connection = self._config
- if requested_connection.port is None:
- return None
- return TransportEndpoint(
- host=requested_connection.host,
- port=requested_connection.port,
- transport_mode=resolve_transport_mode(requested_connection.transport_mode),
- )
-
- def _require_available_endpoint(self, endpoint: TransportEndpoint) -> None:
- endpoint.cleanup_stale_addresses(self._transport_config)
- occupied_ports = endpoint.occupied_ports(self._transport_config)
- if occupied_ports:
- raise UiBridgeEndpointInUseError(endpoint, occupied_ports)
-
- def _write_descriptor_file(self, descriptor: UiBridgeDescriptorFile) -> Path:
- path = AgentRuntimePlatform.resolved_path(
- descriptor.descriptor_file_path
- )
- path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
- path.write_text(
- json.dumps(
- to_jsonable(
- project_dataclass(UiBridgeDescriptorWirePayload, descriptor)
- ),
- sort_keys=True,
- )
- + "\n",
- encoding="utf-8",
- )
- if AgentRuntimePlatform.current().supports_posix_permissions():
- path.chmod(0o600)
- return path
-
- @staticmethod
- def _remove_descriptor_file(path: Path | None) -> None:
- if path is None:
- return
- try:
- path.unlink()
- except FileNotFoundError:
- return
-
- def _bind(self, socket) -> ExecutionConnectionSpec:
- requested_connection = self._config
- mode = resolve_transport_mode(requested_connection.transport_mode)
- port = mode.declaration.bind_socket(
- socket,
- requested_connection.host,
- requested_connection.port,
- self._transport_config,
- )
- return ExecutionConnectionSpec(
- host=requested_connection.host,
- port=port,
- transport_mode=mode,
- persistent=requested_connection.persistent,
- )
-
- def _bind_browser_control_socket(
- self,
- socket,
- connection: ExecutionConnectionSpec,
- ) -> None:
- socket.bind(connection.zmq_control_url(self._transport_config))
-
- def _browser_control_response_payload(
- self,
- request_payload: bytes,
- connection: ExecutionConnectionSpec,
- ) -> bytes:
- try:
- request = ControlRequestHeader.from_wire_payload(request_payload)
- if request.message_type is ControlMessageType.PING:
- return pickle.dumps(self._browser_pong(connection).to_dict())
- raise ValueError(
- f"Unsupported UI bridge browser control message: {request.message_type}"
- )
- except Exception as exc:
- return pickle.dumps(ControlErrorResponse.from_exception(exc).to_dict())
-
- def _browser_pong(
- self,
- connection: ExecutionConnectionSpec,
- ) -> PongResponse:
- control_port = connection.zmq_control_port(self._transport_config)
- return PongResponse(
- port=connection.require_port("UI bridge browser heartbeat"),
- control_port=control_port,
- ready=True,
- server=UI_BRIDGE_BROWSER_SERVER_NAME,
- server_role=ServerRole.GENERIC,
- log_file_path=self._current_log_file_path(),
- application=OPENHCS_ENDPOINT_APPLICATION,
- )
-
- @staticmethod
- def _current_log_file_path() -> str | None:
- try:
- from openhcs.core.log_utils import get_current_log_file_path
-
- return get_current_log_file_path()
- except Exception:
- return None
-
- def __enter__(self) -> "UiBridgeControlServer":
- self.start()
- return self
-
- def __exit__(self, exc_type, exc, traceback) -> None:
- del exc_type, exc, traceback
- self.stop()
diff --git a/openhcs/pyqt_gui/services/ui_bridge_session_actions.py b/openhcs/pyqt_gui/services/ui_bridge_session_actions.py
index d73f4e997b..221f94133d 100644
--- a/openhcs/pyqt_gui/services/ui_bridge_session_actions.py
+++ b/openhcs/pyqt_gui/services/ui_bridge_session_actions.py
@@ -12,16 +12,13 @@
from objectstate.object_state import ObjectStateRegistry
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError
-from openhcs.agent.dto.ui_bridge import (
- UiActionIdentity,
- UiActionInvokeRequest,
- UiActionSummary,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError
+from formbridge.bridge.dto import UiActionIdentity, UiActionInvokeRequest, UiActionSummary
from openhcs.authoring.session.operations import SessionOperation
from openhcs.authoring.session.operations.datasets import PromptedOperation
from openhcs.authoring.session.session import Session
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
+from formbridge.bridge.contracts import (
UiActionDispatch,
UiActionProviderABC,
UiActionProviderIdentity,
diff --git a/openhcs/pyqt_gui/services/ui_bridge_windows.py b/openhcs/pyqt_gui/services/ui_bridge_windows.py
index 0d6dedc5e5..2e00ac9d1f 100644
--- a/openhcs/pyqt_gui/services/ui_bridge_windows.py
+++ b/openhcs/pyqt_gui/services/ui_bridge_windows.py
@@ -1,108 +1,11 @@
-"""Window projection providers for the PyQt UI bridge."""
+"""OpenHCS main-window providers for the UI bridge."""
from __future__ import annotations
-from objectstate.inspection import field_markers, state_markers
-from abc import ABC, abstractmethod
-from collections.abc import Callable
-from dataclasses import dataclass, replace
-from typing import TYPE_CHECKING, ClassVar, TypeAlias
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
-from metaclass_registry import AutoRegisterMeta
-from objectstate import ObjectState
-from PyQt6.QtCore import QModelIndex, Qt
-from PyQt6.QtWidgets import (
- QAbstractItemView,
- QMessageBox,
- QWidget,
-)
-from pyqt_reactive.forms.parameter_form_constants import (
- CONSTANTS as PARAMETER_FORM_CONSTANTS,
-)
-from pyqt_reactive.forms.parameter_form_manager import ParameterFormManager
-from pyqt_reactive.services.scope_window_factory import ScopeWindowRegistry
-from pyqt_reactive.services.scope_window_navigation import ScopeWindowNavigationService
-from pyqt_reactive.services.widget_tree_projection import (
- DEFAULT_WIDGET_DESCRIPTOR_PROJECTOR_REGISTRY,
- ROOT_WIDGET_PATH_ID,
- WIDGET_PATH_SEPARATOR,
- WidgetActionKind,
- WidgetActionTargetInvalidError,
- WidgetActionUnsupportedError,
- WidgetDescriptor,
- WidgetRect,
- WidgetTreeProjection,
- WidgetTreeProjectionService,
-)
-from pyqt_reactive.services.window_manager import WindowManager
-from pyqt_reactive.services.window_navigation import (
- NullWindowNavigationDriver,
- WindowNavigationDispatch,
- RegisteredWindowNavigationCompletion,
- WindowNavigationRequest,
-)
-from pyqt_reactive.services.window_snapshot import (
- QtWindowSnapshotRequest,
- QtWindowSnapshotService,
- QtWindowSnapshot,
- WindowVisualObservation,
-)
-from pyqt_reactive.widgets.shared import (
- BaseFormDialog,
- BaseManagedWindow,
- ManagedWindowAction,
- ManagedWindowActionCapabilities,
-)
-from pyqt_reactive.widgets.shared.abstract_manager_widget import AbstractManagerWidget
-from pyqt_reactive.widgets.shared.list_item_delegate import (
- DIRTY_FIELDS_ROLE,
- OBJECT_STATE_PATH_ROLE,
- SIG_DIFF_FIELDS_ROLE,
-)
-from python_introspect import overlay_non_none_dataclass, project_dataclass
-
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentResourceRef
-from openhcs.agent.services.ui_bridge_service import (
- UiBridgeNavigateWindowOperation,
- UiBridgeSnapshotWindowOperation,
-)
-from openhcs.agent.dto.ui_bridge import (
- DYNAMIC_SCOPE_WINDOW_KIND,
- EMBEDDED_WINDOW_KIND,
- MANAGED_WINDOW_KIND,
- QT_TOP_LEVEL_WINDOW_KIND,
- UiActionIdentity,
- UiActionInvokeRequest,
- UiActionSummary,
- UiLiveOverviewItem,
- UiLiveOverviewMetric,
- UiLiveOverviewSection,
- UiLiveOverviewSeverity,
- UiMutationReceipt,
- UiWidgetActionInvokeRequest,
- UiWidgetActionInvokeResult,
- UiWidgetActionIssueCode,
- UiWidgetActionSemanticCarrier,
- UiWidgetActionSummary,
- UiWidgetRect,
- UiWidgetTreeNode,
- UiWidgetTreeRequest,
- UiWidgetTreeResult,
- UiWindowCatalog,
- UiWindowCloseRequest,
- UiWindowCloseResult,
- UiWindowFocusRequest,
- UiWindowFocusResult,
- UiWindowIdentity,
- UiWindowManagerScope,
- UiWindowNavigateRequest,
- UiWindowNavigateResult,
- UiWindowOperationRequest,
- UiWindowSemanticCarrier,
- UiWindowSnapshotRequest,
- UiWindowSnapshotResult,
- UiWindowSummary,
-)
+from formbridge.bridge.contracts import UiActionProviderIdentity
from openhcs.authoring.session.operations.application import (
CheckForUpdates,
ExitApplication,
@@ -111,2763 +14,23 @@
from openhcs.pyqt_gui.services.ui_bridge_session_actions import (
SessionOperationActionProvider,
)
-from openhcs.agent.ui_bridge_identities import (
- MainWindowWidgetIdentity,
- ManagedWindowWidgetIdentity,
-)
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- UiActionDispatch,
- UiActionProviderABC,
- UiActionProviderIdentity,
- UiLiveOverviewWidget,
- UiWindowProviderABC,
- UiWindowProviderIdentity,
-)
-from openhcs.pyqt_gui.services.ui_bridge_object_state import (
- ObjectStateFieldSemanticProjection,
-)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
- UiBridgeProviderSetABC,
- UiBridgeRegistrationContext,
+from formbridge.bridge.registry import UiBridgeProviderSetABC, UiBridgeRegistrationContext
+from pyqt_reactive.bridge.windows import (
+ MAIN_WINDOW_PROVIDER_ID,
+ ManagedWindowActionProvider,
+ WindowCatalogProjectionABC,
)
-from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
+from openhcs.agent.ui_bridge_identities import MainWindowWidgetIdentity
if TYPE_CHECKING:
from openhcs.pyqt_gui.main import OpenHCSMainWindow
- from pyqt_reactive.widgets.shared.dock_layout import DockPane
-
-WindowRouteCollection: TypeAlias = (
- tuple["EmbeddedWindowRoute", ...] | tuple["ManagedWindowRoute", ...]
-)
-MAIN_WINDOW_PROVIDER_ID = "main_window.windows"
-QT_TOP_LEVEL_PROVIDER_ID = "qt_top_level.windows"
-QT_TOP_LEVEL_WINDOW_ID_PREFIX = "qt_top_level:"
-MANAGED_WINDOW_ACTIONS_TITLE = "Managed window actions"
-MAIN_WINDOW_ACTIONS_TITLE = "Main window actions"
-FIELD_INPUT_ACTION_ROLE = "field_input"
-FIELD_RESET_ACTION_ROLE = "field_reset"
-ITEM_SELECT_ACTION_ROLE = "item_select"
-
-
-def _agent_object_state_scope_id(scope_id: str | None) -> str | None:
- if scope_id is None:
- return None
- return OpenHCSUiWindowId.agent_window_id_for_manager_scope(scope_id)
-
-
-MANAGED_WINDOW_ACTION_PROVIDER_IDENTITY = (
- UiActionProviderIdentity.from_widget_declaration(
- ManagedWindowWidgetIdentity,
- title=MANAGED_WINDOW_ACTIONS_TITLE,
- )
-)
MAIN_WINDOW_ACTION_PROVIDER_IDENTITY = UiActionProviderIdentity.from_widget_declaration(
MainWindowWidgetIdentity,
- title=MAIN_WINDOW_ACTIONS_TITLE,
+ title="Main window actions",
)
-class ManagedWindowSummaryProjection:
- """Project ObjectState-backed managed-window status into window summaries."""
-
- @classmethod
- def project(cls, window: QWidget | None) -> UiWindowSemanticCarrier:
- if not isinstance(window, BaseManagedWindow):
- return UiWindowSemanticCarrier()
- state = window.state
- capabilities = window.managed_window_action_capabilities()
- return UiWindowSemanticCarrier(
- object_state_scope_id=cls._object_state_scope_id(state),
- dirty=bool(state.dirty_fields) if state is not None else False,
- signature_diff=(
- bool(state.signature_diff_fields) if state is not None else False
- ),
- dirty_field_count=(len(state.dirty_fields) if state is not None else 0),
- signature_diff_field_count=(
- len(state.signature_diff_fields) if state is not None else 0
- ),
- semantic_markers=cls._semantic_markers(state),
- managed_action_ids=cls._managed_action_ids(capabilities),
- )
-
- @staticmethod
- def _object_state_scope_id(state: ObjectState | None) -> str | None:
- if state is None:
- return None
- return _agent_object_state_scope_id(state.scope_id)
-
- @staticmethod
- def _semantic_markers(state: ObjectState | None) -> tuple[str, ...]:
- return () if state is None else state_markers(state)
-
- @staticmethod
- def _managed_action_ids(
- capabilities: ManagedWindowActionCapabilities,
- ) -> tuple[str, ...]:
- return tuple(
- action.value
- for action in ManagedWindowAction
- if action.is_supported(capabilities)
- )
-
-
-class EmbeddedManagerSummaryProjection:
- """Project shared AbstractManagerWidget row semantics into window summaries."""
-
- @classmethod
- def project(cls, widget: QWidget | None) -> UiWindowSemanticCarrier:
- if not isinstance(widget, AbstractManagerWidget):
- return UiWindowSemanticCarrier()
- item_list = widget.item_list
- if item_list is None:
- return UiWindowSemanticCarrier()
- model = item_list.model()
- if model is None:
- return UiWindowSemanticCarrier()
-
- dirty_field_count = 0
- signature_diff_field_count = 0
- for row_index in range(model.rowCount()):
- item_record = _WidgetItemSemanticRecord.from_index(
- model.index(row_index, 0)
- )
- dirty_field_count += len(item_record.dirty_fields)
- signature_diff_field_count += len(item_record.signature_diff_fields)
-
- return UiWindowSemanticCarrier(
- dirty=bool(dirty_field_count),
- signature_diff=bool(signature_diff_field_count),
- dirty_field_count=dirty_field_count,
- signature_diff_field_count=signature_diff_field_count,
- semantic_markers=cls._semantic_markers(
- dirty_field_count=dirty_field_count,
- signature_diff_field_count=signature_diff_field_count,
- ),
- )
-
- @staticmethod
- def _semantic_markers(
- *,
- dirty_field_count: int,
- signature_diff_field_count: int,
- ) -> tuple[str, ...]:
- return field_markers(
- dirty=bool(dirty_field_count),
- signature_diff=bool(signature_diff_field_count),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class WindowProjectionTarget:
- """Resolved Qt widget plus its agent-facing summary."""
-
- widget: QWidget
- summary: UiWindowSummary
-
-
-class WindowProjectionResultAuthority:
- """Shared agent-result fragments for UI window projection operations."""
-
- @staticmethod
- def unknown_window(identity: UiWindowIdentity) -> AgentError:
- return AgentError(
- code="unknown_ui_window",
- message=f"Unknown or closed UI window: {identity.window_id!r}",
- )
-
- @staticmethod
- def unsupported_navigation_target(request: UiWindowNavigateRequest) -> AgentError:
- return AgentError(
- code="ui_window_navigation_target_unsupported",
- message=(
- f"UI window {request.window_id!r} does not own the requested "
- "item or field target."
- ),
- path=request.field_path,
- )
-
-
-class WindowCloseResultBoundaryPolicy:
- """Single result boundary for UI window close operations."""
-
- @staticmethod
- def closed_summary(summary: UiWindowSummary) -> UiWindowSummary:
- return replace(summary, visible=False, focusable=False)
-
- @classmethod
- def closed(
- cls,
- request: UiWindowCloseRequest,
- summary: UiWindowSummary,
- ) -> UiWindowCloseResult:
- return UiWindowCloseResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- closed=True,
- summary=cls.closed_summary(summary),
- )
-
- @staticmethod
- def error(
- request: UiWindowCloseRequest,
- code: str,
- message: str,
- *,
- summary: UiWindowSummary | None = None,
- ) -> UiWindowCloseResult:
- return UiWindowCloseResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- closed=False,
- summary=summary,
- errors=(AgentError(code=code, message=message),),
- )
-
-
-class MainWindowContextFactoryMixin:
- """Construct registry leaves from an optional OpenHCS main-window context."""
-
- @classmethod
- def create(cls, main_window: "OpenHCSMainWindow | None"):
- return cls(main_window)
-
-
-class WindowCatalogProjectionABC(
- MainWindowContextFactoryMixin,
- UiWindowProviderABC,
- ABC,
- metaclass=AutoRegisterMeta,
-):
- """Provider/projection authority for one catalog of UI windows."""
-
- __registry_key__ = "projection_id"
- __skip_if_no_key__ = True
-
- projection_id: ClassVar[str | None] = None
- identity: ClassVar[UiWindowProviderIdentity]
-
- @classmethod
- def registered_types(cls) -> tuple[type["WindowCatalogProjectionABC"], ...]:
- return tuple(cls.__registry__.values())
-
- @classmethod
- def for_projection_id(
- cls,
- projection_id: str,
- main_window: "OpenHCSMainWindow | None",
- ) -> "WindowCatalogProjectionABC":
- return cls.__registry__[projection_id].create(main_window)
-
- def catalog(self) -> UiWindowCatalog:
- return UiWindowCatalog(
- schema_version=SCHEMA_VERSION,
- windows=self.summaries(),
- )
-
- def overview_sections(self) -> tuple[UiLiveOverviewSection, ...]:
- windows = self.summaries()
- error_dialogs = tuple(
- window
- for window in windows
- if window.error_dialog
- )
- return (
- UiLiveOverviewSection(
- section_id=self.identity.provider_id,
- title=self.identity.title,
- summary=f"{len(windows)} windows",
- metrics=(
- UiLiveOverviewMetric(
- key="windows",
- label="windows",
- value=str(len(windows)),
- ),
- UiLiveOverviewMetric(
- key="visible",
- label="visible",
- value=str(sum(1 for window in windows if window.visible)),
- ),
- UiLiveOverviewMetric(
- key="error_dialogs",
- label="error dialogs",
- value=str(len(error_dialogs)),
- ),
- ),
- items=tuple(self._overview_window_item(window) for window in windows),
- ),
- )
-
- @staticmethod
- def _overview_window_item(window: UiWindowSummary) -> UiLiveOverviewItem:
- is_error_dialog = window.error_dialog
- return UiLiveOverviewItem(
- label=window.title,
- status=window.window_kind,
- detail=f"visible={window.visible} focusable={window.focusable}",
- severity=(
- UiLiveOverviewSeverity.ERROR.value
- if is_error_dialog
- else UiLiveOverviewSeverity.INFO.value
- ),
- source_window_id=window.window_id,
- )
-
- @abstractmethod
- def summaries(self) -> tuple[UiWindowSummary, ...]:
- """Return the windows currently visible through this projection."""
- raise NotImplementedError
-
- @abstractmethod
- def handles(self, window_id: str) -> bool:
- """Return whether this projection owns a window id."""
- raise NotImplementedError
-
- @abstractmethod
- def focus(self, request: UiWindowFocusRequest) -> UiWindowFocusResult:
- raise NotImplementedError
-
- @abstractmethod
- def navigate(
- self,
- request: UiWindowNavigateRequest,
- completed: Callable[[UiWindowNavigateResult], None] | None = None,
- ) -> UiWindowNavigateResult:
- raise NotImplementedError
-
- @abstractmethod
- def close(self, request: UiWindowCloseRequest) -> UiWindowCloseResult:
- raise NotImplementedError
-
- @abstractmethod
- def snapshot(
- self,
- request: UiWindowSnapshotRequest,
- completed: Callable[[UiWindowSnapshotResult], None] | None = None,
- ) -> UiWindowSnapshotResult:
- raise NotImplementedError
-
- @abstractmethod
- def widget_tree(self, request: UiWidgetTreeRequest) -> UiWidgetTreeResult:
- raise NotImplementedError
-
- @abstractmethod
- def invoke_widget_action(
- self,
- request: UiWidgetActionInvokeRequest,
- ) -> UiWidgetActionInvokeResult:
- raise NotImplementedError
-
-
-class WindowTargetOperationProjectionMixin(ABC):
- """Shared target-backed window operations for projection providers."""
-
- @abstractmethod
- def target_for_operation(
- self,
- request: UiWindowOperationRequest,
- ) -> WindowProjectionTarget | None:
- raise NotImplementedError
-
- def snapshot(
- self,
- request: UiWindowSnapshotRequest,
- completed: Callable[[UiWindowSnapshotResult], None] | None = None,
- ) -> UiWindowSnapshotResult:
- target = self.target_for_operation(request)
- snapshot_results = UiWindowSnapshotResultFactory()
- if target is None:
- return snapshot_results.error(
- request,
- WindowProjectionResultAuthority.unknown_window(request),
- )
- return snapshot_results.capture(request, target, completed)
-
- def widget_tree(self, request: UiWidgetTreeRequest) -> UiWidgetTreeResult:
- target = self.target_for_operation(request)
- widget_tree_results = UiWidgetTreeResultFactory()
- if target is None:
- return widget_tree_results.error(
- request,
- WindowProjectionResultAuthority.unknown_window(request),
- )
- return widget_tree_results.project(request, target)
-
- def invoke_widget_action(
- self,
- request: UiWidgetActionInvokeRequest,
- ) -> UiWidgetActionInvokeResult:
- target = self.target_for_operation(request)
- invoke_results = UiWidgetActionInvokeResultFactory()
- if target is None:
- return invoke_results.error(
- request,
- WindowProjectionResultAuthority.unknown_window(request),
- )
- return invoke_results.invoke(request, target)
-
-
-@dataclass(frozen=True, slots=True)
-class FocusableWindowRouteMixin(ABC):
- """Shared focus algorithm for static focusable window routes."""
-
- focus_action: Callable[[], None]
-
- @abstractmethod
- def summary(self) -> UiWindowSummary:
- raise NotImplementedError
-
- @abstractmethod
- def target(self, *, create_if_missing: bool) -> WindowProjectionTarget | None:
- raise NotImplementedError
-
- def focus(self) -> UiWindowSummary:
- self.focus_action()
- return self.summary()
-
-
-@dataclass(frozen=True, slots=True)
-class EmbeddedWindowRoute:
- """Focusable embedded widget route owned by the main window layout."""
-
- pane: "DockPane"
-
- @property
- def identity(self) -> UiWindowIdentity:
- return UiWindowIdentity(window_id=self.pane.window_id)
-
- @property
- def title(self) -> str:
- return self.pane.title
-
- def widget(self) -> QWidget:
- return self.pane.widget
-
- def focus(self) -> UiWindowSummary:
- self.pane.show()
- return self.summary()
-
- def navigate(
- self,
- request: UiWindowNavigateRequest,
- completed: Callable[[RegisteredWindowNavigationCompletion], None] | None = None,
- ) -> WindowNavigationDispatch:
- self.pane.show()
- widget = self.widget()
- driver = (
- widget.window_navigation_driver()
- if isinstance(widget, AbstractManagerWidget)
- else NullWindowNavigationDriver()
- )
- return WindowManager.dispatch_widget_navigation(
- widget,
- driver,
- requested_scope_id=request.window_id,
- item_id=request.item_id,
- field_path=request.field_path,
- completed=completed,
- )
-
- def overview_sections(self) -> tuple[UiLiveOverviewSection, ...]:
- widget = self.widget()
- if isinstance(widget, UiLiveOverviewWidget):
- return widget.overview_sections()
- return ()
-
- def target(self, *, create_if_missing: bool) -> WindowProjectionTarget | None:
- del create_if_missing
- return WindowProjectionTarget(
- widget=self.widget(),
- summary=self.summary(),
- )
-
- def summary(self) -> UiWindowSummary:
- widget = self.widget()
- scope = UiWindowManagerScope.from_identity(self.identity)
- return project_dataclass(
- UiWindowSummary,
- EmbeddedManagerSummaryProjection.project(widget),
- schema_version=SCHEMA_VERSION,
- identity=self.identity,
- title=self.title,
- window_kind=EMBEDDED_WINDOW_KIND,
- visible=widget.isVisible(),
- focusable=True,
- manager_scope=scope,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class ManagedWindowRoute(FocusableWindowRouteMixin):
- """Focusable WindowManager-backed static window route."""
-
- identity: UiWindowIdentity
- title: str
-
- def navigate(
- self,
- request: UiWindowNavigateRequest,
- completed: Callable[[RegisteredWindowNavigationCompletion], None] | None = None,
- ) -> WindowNavigationDispatch:
- self.focus()
- return WindowManager.focus_and_navigate_result(
- UiWindowManagerScope.from_identity(self.identity).value,
- item_id=request.item_id,
- field_path=request.field_path,
- requested_scope_id=request.window_id,
- completed=completed,
- )
-
- def widget(self, create_if_missing: bool) -> QWidget | None:
- scope = UiWindowManagerScope.from_identity(self.identity)
- window = WindowManager.get_window(scope.value)
- if window is not None:
- return window
- if not create_if_missing:
- return None
- self.focus_action()
- return WindowManager.get_window(scope.value)
-
- def target(self, *, create_if_missing: bool) -> WindowProjectionTarget | None:
- widget = self.widget(create_if_missing=create_if_missing)
- if widget is None:
- return None
- return WindowProjectionTarget(
- widget=widget,
- summary=self.summary(),
- )
-
- def summary(self) -> UiWindowSummary:
- scope = UiWindowManagerScope.from_identity(self.identity)
- window = WindowManager.get_window(scope.value)
- visible = False
- if window is not None:
- visible = window.isVisible()
- return project_dataclass(
- UiWindowSummary,
- ManagedWindowSummaryProjection.project(window),
- schema_version=SCHEMA_VERSION,
- identity=self.identity,
- title=self.title,
- window_kind=MANAGED_WINDOW_KIND,
- visible=visible,
- focusable=True,
- manager_scope=scope,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class WindowIdentitySet:
- """Closed set of window identities for static route filtering."""
-
- identities: frozenset[UiWindowIdentity]
-
- @classmethod
- def from_routes(
- cls,
- routes: WindowRouteCollection,
- ) -> "WindowIdentitySet":
- return cls(frozenset(route.identity for route in routes))
-
- def contains(self, identity: UiWindowIdentity) -> bool:
- return identity in self.identities
-
- def union(self, other: "WindowIdentitySet") -> "WindowIdentitySet":
- return WindowIdentitySet(self.identities | other.identities)
-
-
-@dataclass(frozen=True, slots=True)
-class WindowRouteIndex:
- """Identity-indexed route lookup for static window routes."""
-
- embedded_routes: tuple[EmbeddedWindowRoute, ...]
- managed_routes: tuple[ManagedWindowRoute, ...]
-
- @property
- def static_identities(self) -> WindowIdentitySet:
- return WindowIdentitySet.from_routes(self.embedded_routes).union(
- WindowIdentitySet.from_routes(self.managed_routes)
- )
-
- def embedded_route(self, identity: UiWindowIdentity) -> EmbeddedWindowRoute | None:
- return self._route_by_identity(self.embedded_routes, identity)
-
- def managed_route(self, identity: UiWindowIdentity) -> ManagedWindowRoute | None:
- return self._route_by_identity(self.managed_routes, identity)
-
- @staticmethod
- def _route_by_identity(
- routes: WindowRouteCollection,
- identity: UiWindowIdentity,
- ):
- for route in routes:
- if route.identity == identity:
- return route
- return None
-
-
-@dataclass(frozen=True, slots=True)
-class WindowRouteResolution:
- """A static window route resolved for a requested agent-visible identity."""
-
- request_identity: UiWindowIdentity
- route_identity: UiWindowIdentity
- route: EmbeddedWindowRoute | ManagedWindowRoute
-
- def summary(self, target_summary: UiWindowSummary | None = None) -> UiWindowSummary:
- if target_summary is None:
- target_summary = self.route.summary()
- if self.route_identity == self.request_identity:
- return target_summary
- return replace(target_summary, identity=self.request_identity)
-
- def focus(self) -> UiWindowSummary:
- return self.summary(self.route.focus())
-
- def target(self, *, create_if_missing: bool) -> WindowProjectionTarget | None:
- target = self.route.target(create_if_missing=create_if_missing)
- if target is None:
- return None
- return WindowProjectionTarget(
- widget=target.widget,
- summary=self.summary(target.summary),
- )
-
-
-class EmbeddedWindowProjection:
- """Project embedded main-window widgets."""
-
- def __init__(self, main_window: "OpenHCSMainWindow") -> None:
- self._main_window = main_window
-
- def routes(self) -> tuple[EmbeddedWindowRoute, ...]:
- embedded = self._main_window.embedded_widgets
- return tuple(EmbeddedWindowRoute(pane=pane) for pane in embedded.panes())
-
-
-class ManagedWindowProjection:
- """Project static WindowManager-managed windows."""
-
- def __init__(self, main_window: "OpenHCSMainWindow") -> None:
- self._main_window = main_window
-
- def routes(
- self,
- embedded_routes: tuple[EmbeddedWindowRoute, ...],
- ) -> tuple[ManagedWindowRoute, ...]:
- embedded_identities = WindowIdentitySet.from_routes(embedded_routes)
- return tuple(
- ManagedWindowRoute(
- identity=UiWindowIdentity(window_id=spec.window_id),
- title=spec.title,
- focus_action=self._focus_static_window_action(spec.window_id),
- )
- for spec in self._main_window.window_specs.values()
- if not embedded_identities.contains(
- UiWindowIdentity(window_id=spec.window_id)
- )
- )
-
- def _focus_static_window_action(self, window_id: str) -> Callable[[], None]:
- def focus() -> None:
- self._main_window.show_window(window_id, hide_if_startup=False)
-
- return focus
-
-
-class DynamicScopeWindowProjection:
- """Project already-open dynamic ObjectState/scope windows."""
-
- def summaries(
- self,
- route_index: WindowRouteIndex,
- ) -> tuple[UiWindowSummary, ...]:
- return tuple(
- self.summary(
- self._identity_for_manager_scope(scope_id),
- scope_id,
- )
- for scope_id in WindowManager.get_open_scopes()
- if not route_index.static_identities.contains(
- self._identity_for_manager_scope(scope_id)
- )
- )
-
- def summary(
- self,
- identity: UiWindowIdentity,
- manager_scope_id: str | None = None,
- ) -> UiWindowSummary:
- manager_identity = identity
- if manager_scope_id is not None:
- manager_identity = UiWindowIdentity(window_id=manager_scope_id)
- scope = UiWindowManagerScope.from_identity(manager_identity)
- window = WindowManager.get_window(scope.value)
- title = scope.value
- visible = False
- if window is not None:
- title = window.windowTitle()
- visible = window.isVisible()
- return project_dataclass(
- UiWindowSummary,
- ManagedWindowSummaryProjection.project(window),
- schema_version=SCHEMA_VERSION,
- identity=identity,
- title=title,
- window_kind=DYNAMIC_SCOPE_WINDOW_KIND,
- visible=visible,
- focusable=window is not None,
- manager_scope=scope,
- )
-
- @staticmethod
- def _identity_for_manager_scope(scope_id: str) -> UiWindowIdentity:
- return UiWindowIdentity(
- window_id=OpenHCSUiWindowId.agent_window_id_for_manager_scope(scope_id)
- )
-
-
-@dataclass(frozen=True, slots=True)
-class OpenWindowManagerScope:
- """Witness for an agent identity resolved to an open WindowManager scope."""
-
- identity: UiWindowIdentity
- scope_id: str
- widget: QWidget
-
- def target(
- self,
- dynamic_projection: DynamicScopeWindowProjection,
- ) -> WindowProjectionTarget:
- return WindowProjectionTarget(
- widget=self.widget,
- summary=dynamic_projection.summary(self.identity, self.scope_id),
- )
-
-
-class ScopeWindowTargetOperationProjectionABC(
- WindowTargetOperationProjectionMixin,
- ABC,
-):
- """Shared target lookup for WindowManager and scope-registry projections."""
-
- @abstractmethod
- def _route_index(self) -> WindowRouteIndex:
- raise NotImplementedError
-
- @abstractmethod
- def _dynamic_scope_projection(self) -> DynamicScopeWindowProjection:
- raise NotImplementedError
-
- def target_for_operation(
- self,
- request: UiWindowOperationRequest,
- ) -> WindowProjectionTarget | None:
- identity = request.as_identity()
- return self._target(
- identity,
- create_if_missing=request.open_policy.create_if_missing,
- )
-
- def _target(
- self,
- identity: UiWindowIdentity,
- *,
- create_if_missing: bool,
- ) -> WindowProjectionTarget | None:
- route_index = self._route_index()
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if embedded_route is not None:
- return embedded_route.target(create_if_missing=create_if_missing)
-
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if managed_route is not None:
- return managed_route.target(create_if_missing=create_if_missing)
-
- open_target = self._open_dynamic_target(identity)
- if open_target is not None:
- return open_target
-
- embedded_alias = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if embedded_alias is not None:
- return embedded_alias.target(create_if_missing=create_if_missing)
-
- managed_alias = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if managed_alias is not None:
- return managed_alias.target(create_if_missing=create_if_missing)
-
- scope = UiWindowManagerScope.from_identity(
- self._resolved_window_identity(identity)
- )
- scope_widget = WindowManager.get_window(scope.value)
- if scope_widget is not None:
- return WindowProjectionTarget(
- widget=scope_widget,
- summary=self._dynamic_scope_projection().summary(
- identity,
- scope.value,
- ),
- )
-
- if (
- create_if_missing
- and ScopeWindowRegistry.find_handler(identity.window_id) is not None
- ):
- navigation = ScopeWindowNavigationService.navigate(
- WindowNavigationRequest(
- scope_id=identity.window_id,
- create_if_missing=True,
- )
- )
- if navigation.focused:
- target_scope_id = navigation.window_scope_id or scope.value
- scope_widget = WindowManager.get_window(target_scope_id)
- if scope_widget is not None:
- return WindowProjectionTarget(
- widget=scope_widget,
- summary=self._dynamic_scope_projection().summary(
- identity,
- target_scope_id,
- ),
- )
-
- return None
-
- def _embedded_route_resolution(
- self,
- identity: UiWindowIdentity,
- route_index: WindowRouteIndex,
- *,
- resolve_scope_alias: bool,
- ) -> WindowRouteResolution | None:
- return self._static_route_resolution(
- identity,
- route_index.embedded_route,
- resolve_scope_alias=resolve_scope_alias,
- )
-
- def _managed_route_resolution(
- self,
- identity: UiWindowIdentity,
- route_index: WindowRouteIndex,
- *,
- resolve_scope_alias: bool,
- ) -> WindowRouteResolution | None:
- return self._static_route_resolution(
- identity,
- route_index.managed_route,
- resolve_scope_alias=resolve_scope_alias,
- )
-
- def _static_route_resolution(
- self,
- identity: UiWindowIdentity,
- route_for_identity: Callable[
- [UiWindowIdentity],
- EmbeddedWindowRoute | ManagedWindowRoute | None,
- ],
- *,
- resolve_scope_alias: bool,
- ) -> WindowRouteResolution | None:
- route_identity = identity
- if resolve_scope_alias:
- route_identity = self._resolved_window_identity(identity)
- if route_identity == identity:
- return None
-
- route = route_for_identity(route_identity)
- if route is None:
- return None
- return WindowRouteResolution(
- request_identity=identity,
- route_identity=route_identity,
- route=route,
- )
-
- def _open_dynamic_target(
- self,
- identity: UiWindowIdentity,
- ) -> WindowProjectionTarget | None:
- open_scope = self._open_window_manager_scope(identity)
- if open_scope is None:
- return None
- return open_scope.target(self._dynamic_scope_projection())
-
- @classmethod
- def _open_window_manager_scope_id(cls, identity: UiWindowIdentity) -> str | None:
- open_scope = cls._open_window_manager_scope(identity)
- if open_scope is None:
- return None
- return open_scope.scope_id
-
- @staticmethod
- def _open_window_manager_scope(
- identity: UiWindowIdentity,
- ) -> OpenWindowManagerScope | None:
- for scope_id in OpenHCSUiWindowId.manager_scopes_for_agent_window_id(
- identity.window_id
- ):
- widget = WindowManager.get_window(scope_id)
- if widget is not None:
- return OpenWindowManagerScope(
- identity=identity,
- scope_id=scope_id,
- widget=widget,
- )
- return None
-
- @staticmethod
- def _resolved_window_identity(identity: UiWindowIdentity) -> UiWindowIdentity:
- route = ScopeWindowRegistry.find_handler(identity.window_id)
- if route is None:
- return identity
- target = route.navigation_target(identity.window_id)
- return UiWindowIdentity(window_id=target.window_scope_id)
-
-
-class QtTopLevelWindowProjection(
- WindowTargetOperationProjectionMixin,
- WindowCatalogProjectionABC,
-):
- """Project visible Qt top-level windows that are not WindowManager scopes."""
-
- projection_id = QT_TOP_LEVEL_PROVIDER_ID
- identity = UiWindowProviderIdentity(
- provider_id=QT_TOP_LEVEL_PROVIDER_ID,
- title="Qt top-level windows",
- )
-
- def __init__(
- self,
- main_window: "OpenHCSMainWindow | None",
- ) -> None:
- self._main_window = main_window
-
- def summaries(self) -> tuple[UiWindowSummary, ...]:
- summaries = []
- for widget in self._projected_top_level_widgets():
- summary = WindowManager.project_live_window(widget, self.summary)
- if summary is not None:
- summaries.append(summary)
- return tuple(summaries)
-
- def handles(self, window_id: str) -> bool:
- return self.target(UiWindowIdentity(window_id=window_id)) is not None
-
- def target(self, identity: UiWindowIdentity) -> WindowProjectionTarget | None:
- identity = identity.as_identity()
- for widget in self._projected_top_level_widgets():
- target = WindowManager.project_live_window(
- widget,
- lambda live_widget: self._matching_target(live_widget, identity),
- )
- if target is not None:
- return target
- return None
-
- def _matching_target(
- self,
- widget: QWidget,
- identity: UiWindowIdentity,
- ) -> WindowProjectionTarget | None:
- if self._identity(widget) != identity:
- return None
- return WindowProjectionTarget(widget=widget, summary=self.summary(widget))
-
- def target_for_operation(
- self,
- request: UiWindowOperationRequest,
- ) -> WindowProjectionTarget | None:
- return self.target(request)
-
- def focus(self, request: UiWindowFocusRequest) -> UiWindowFocusResult:
- target = self.target(request)
- if target is None:
- return UiWindowFocusResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=False,
- errors=(WindowProjectionResultAuthority.unknown_window(request),),
- )
- target.widget.show()
- target.widget.raise_()
- target.widget.activateWindow()
- return UiWindowFocusResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=True,
- summary=self.summary(target.widget),
- )
-
- def navigate(
- self,
- request: UiWindowNavigateRequest,
- completed: Callable[[UiWindowNavigateResult], None] | None = None,
- ) -> UiWindowNavigateResult:
- focus_result = self.focus(
- UiWindowFocusRequest(
- window_id=request.window_id,
- open_policy=request.open_policy,
- )
- )
- return UiWindowNavigateResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=focus_result.focused,
- navigated=False,
- created=False,
- summary=focus_result.summary,
- errors=(
- focus_result.errors
- or (
- (
- WindowProjectionResultAuthority.unsupported_navigation_target(
- request
- ),
- )
- if request.has_target
- else ()
- )
- ),
- warnings=focus_result.warnings,
- )
-
- def close(self, request: UiWindowCloseRequest) -> UiWindowCloseResult:
- target = self.target(request)
- if target is None:
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "unknown_ui_window",
- f"Unknown or closed UI window: {request.window_id!r}",
- )
- if target.widget is self._main_window:
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "ui_window_close_unsupported",
- "The OpenHCS main window cannot be closed through this UI bridge operation.",
- summary=target.summary,
- )
- if target.widget.close():
- return WindowCloseResultBoundaryPolicy.closed(
- request,
- target.summary,
- )
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "ui_window_close_rejected",
- f"Qt rejected the close request for UI window: {request.window_id!r}",
- summary=target.summary,
- )
-
- def summary(self, widget: QWidget) -> UiWindowSummary:
- identity = self._identity(widget)
- return UiWindowSummary(
- schema_version=SCHEMA_VERSION,
- identity=identity,
- title=self._title(widget),
- window_kind=QT_TOP_LEVEL_WINDOW_KIND,
- visible=widget.isVisible(),
- focusable=True,
- manager_scope=None,
- error_dialog=self._is_error_dialog(widget),
- )
-
- def _projected_top_level_widgets(self) -> tuple[QWidget, ...]:
- excluded_widgets = self._excluded_widgets()
- return tuple(
- widget
- for widget in WindowManager.visible_top_level_windows()
- if widget not in excluded_widgets
- )
-
- def _excluded_widgets(self) -> frozenset[QWidget]:
- widgets: set[QWidget] = set()
- for scope_id in WindowManager.get_open_scopes():
- widget = WindowManager.get_window(scope_id)
- if widget is not None:
- top_level_widget = widget.window()
- if top_level_widget is not self._main_window:
- widgets.add(top_level_widget)
- if self._main_window is not None:
- widgets.update(
- pane.dock_widget
- for pane in self._main_window.embedded_widgets.panes()
- if pane.dock_widget.isFloating()
- )
- return frozenset(widgets)
-
- def _identity(self, widget: QWidget) -> UiWindowIdentity:
- if widget is self._main_window:
- return UiWindowIdentity(window_id=OpenHCSUiWindowId.main_window)
- return UiWindowIdentity(
- window_id=f"{QT_TOP_LEVEL_WINDOW_ID_PREFIX}{int(widget.winId())}"
- )
-
- @staticmethod
- def _title(widget: QWidget) -> str:
- title = widget.windowTitle()
- if title:
- return title
- return type(widget).__name__
-
- @staticmethod
- def _is_error_dialog(widget: QWidget) -> bool:
- return (
- isinstance(widget, QMessageBox)
- and widget.icon() is QMessageBox.Icon.Critical
- )
-
-
-class UiWindowSnapshotResultFactory:
- """Build UI bridge screenshot results from resolved Qt window targets."""
-
- def __init__(self) -> None:
- self._snapshotter = QtWindowSnapshotService()
-
- def capture(
- self,
- request: UiWindowSnapshotRequest,
- target: WindowProjectionTarget,
- completed: Callable[[UiWindowSnapshotResult], None] | None = None,
- ) -> UiWindowSnapshotResult:
- qt_request = QtWindowSnapshotRequest(
- widget=target.widget,
- capture=request,
- subject_id=request.window_id,
- title=target.summary.title,
- )
- try:
- if request.frame_condition.observes:
- if completed is None:
- raise ValueError(
- "Observed snapshots require an operation completion owner."
- )
- self._snapshotter.observe(
- qt_request,
- lambda snapshot: completed(
- self.from_snapshot(request, target, snapshot)
- ),
- lambda failure: completed(
- self.error(
- request,
- AgentError.from_exception(
- UiBridgeSnapshotWindowOperation.failure_error_code,
- failure.error,
- ),
- summary=target.summary,
- observation=failure.observation,
- )
- ),
- )
- return project_dataclass(
- UiWindowSnapshotResult,
- request,
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- captured=False,
- summary=target.summary,
- )
- snapshot = self._snapshotter.capture(qt_request)
- except Exception as exc:
- return self.error(
- request,
- AgentError.from_exception(
- UiBridgeSnapshotWindowOperation.failure_error_code, exc
- ),
- summary=target.summary,
- )
- return self.from_snapshot(request, target, snapshot)
-
- @staticmethod
- def from_snapshot(
- request: UiWindowSnapshotRequest,
- target: WindowProjectionTarget,
- snapshot: QtWindowSnapshot,
- ) -> UiWindowSnapshotResult:
- return project_dataclass(
- UiWindowSnapshotResult,
- request,
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- captured=True,
- resource=AgentResourceRef(
- uri=snapshot.uri,
- title=snapshot.title,
- mime_type=snapshot.mime_type,
- path=snapshot.path,
- size_bytes=snapshot.size_bytes,
- sha256=snapshot.sha256,
- ),
- summary=target.summary,
- width=snapshot.width,
- height=snapshot.height,
- observation=snapshot.observation,
- )
-
- @staticmethod
- def error(
- request: UiWindowSnapshotRequest,
- error: AgentError,
- *,
- summary: UiWindowSummary | None = None,
- observation: WindowVisualObservation | None = None,
- ) -> UiWindowSnapshotResult:
- return project_dataclass(
- UiWindowSnapshotResult,
- request,
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- captured=False,
- summary=summary,
- observation=observation,
- errors=(error,),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _ItemViewIndexResolution:
- view: QAbstractItemView
- index: QModelIndex
-
-
-class UiWidgetPathResolver:
- """Resolve widget-tree path ids against live Qt widgets and descriptors."""
-
- @classmethod
- def path_from_id(cls, path_id: str) -> tuple[int, ...]:
- if path_id == ROOT_WIDGET_PATH_ID:
- return ()
- if not path_id:
- raise ValueError("Widget path_id must not be empty.")
- parts = path_id.split(WIDGET_PATH_SEPARATOR)
- path: list[int] = []
- for part in parts:
- try:
- index = int(part)
- except ValueError as exc:
- raise ValueError(f"Invalid widget path component: {part!r}") from exc
- if index < 0:
- raise ValueError(
- f"Widget path component must be non-negative: {part!r}"
- )
- path.append(index)
- return tuple(path)
-
- @classmethod
- def widget_at_path(cls, widget: QWidget, path: tuple[int, ...]) -> QWidget | None:
- current = widget
- for child_index in path:
- children = cls.direct_children(current)
- if child_index >= len(children):
- return None
- current = children[child_index]
- return current
-
- @classmethod
- def descriptor_at_path(
- cls,
- descriptor: WidgetDescriptor,
- path: tuple[int, ...],
- ) -> WidgetDescriptor | None:
- current = descriptor
- for child_index in path:
- child = current.child_at_index(child_index)
- if child is None:
- return None
- current = child
- return current
-
- @staticmethod
- def direct_children(widget: QWidget) -> tuple[QWidget, ...]:
- return tuple(
- widget.findChildren(
- QWidget,
- options=Qt.FindChildOption.FindDirectChildrenOnly,
- )
- )
-
- @classmethod
- def item_view_index_at_path(
- cls,
- widget: QWidget,
- path: tuple[int, ...],
- ) -> "_ItemViewIndexResolution | None":
- current_widget = widget
- current_index = QModelIndex()
- active_view: QAbstractItemView | None = None
-
- for child_index in path:
- if active_view is None:
- children = cls.direct_children(current_widget)
- if child_index < len(children):
- current_widget = children[child_index]
- continue
- if not isinstance(current_widget, QAbstractItemView):
- return None
- active_view = current_widget
- model_row = child_index - len(children)
- else:
- model_row = child_index
-
- model = active_view.model()
- if model is None:
- return None
- current_index = model.index(model_row, 0, current_index)
- if not current_index.isValid():
- return None
-
- if active_view is None:
- return None
- return _ItemViewIndexResolution(active_view, current_index)
-
-
-class UiWidgetActionInvokeResultFactory:
- """Invoke projected widget actions through live Qt widgets."""
-
- AUTO_ACTION_KIND = "auto"
-
- def invoke(
- self,
- request: UiWidgetActionInvokeRequest,
- target: WindowProjectionTarget,
- ) -> UiWidgetActionInvokeResult:
- try:
- projection = WidgetTreeProjectionService.project(target.widget)
- path = UiWidgetPathResolver.path_from_id(request.path_id)
- except Exception as exc:
- return self.error(
- request,
- AgentError.from_exception(
- UiWidgetActionIssueCode.RESOLUTION_FAILED.value,
- exc,
- ),
- summary=target.summary,
- )
-
- descriptor = UiWidgetPathResolver.descriptor_at_path(projection.root, path)
- if descriptor is None:
- return self.error(
- request,
- AgentError(
- code=UiWidgetActionIssueCode.WIDGET_UNKNOWN.value,
- message=(
- f"Widget path_id {request.path_id!r} was not found in "
- f"window {request.window_id!r}."
- ),
- hint="Refresh the widget tree and retry with a current path_id.",
- ),
- summary=target.summary,
- )
-
- action_summary = UiWidgetTreeResultFactory.action_summary(
- descriptor,
- field_semantics=_WidgetFieldSemanticContext.from_target(target),
- item_semantics=_WidgetItemSemanticContext.from_target(target),
- )
-
- action_kind = self._resolve_action_kind(request, descriptor)
- if isinstance(action_kind, AgentError):
- return self.error(request, action_kind, summary=action_summary)
-
- if action_kind.item_action is not None:
- return self._invoke_item_action(
- request,
- target,
- path,
- descriptor,
- action_summary,
- action_kind=action_kind,
- )
-
- widget = UiWidgetPathResolver.widget_at_path(target.widget, path)
- if widget is None:
- return self.error(
- request,
- AgentError(
- code=UiWidgetActionIssueCode.WIDGET_UNKNOWN.value,
- message=(
- f"Widget path_id {request.path_id!r} was not found in "
- f"window {request.window_id!r}."
- ),
- hint="Refresh the widget tree and retry with a current path_id.",
- ),
- summary=action_summary,
- )
-
- guard_error = self._guard_error(request, descriptor, widget, action_kind)
- if guard_error is not None:
- return self.error(
- request,
- guard_error,
- summary=action_summary,
- action_kind=action_kind.value,
- )
-
- return self._invoke_projector_action(
- request,
- action_summary,
- widget,
- action_kind=action_kind,
- )
-
- def _invoke_projector_action(
- self,
- request: UiWidgetActionInvokeRequest,
- action_summary: UiWidgetActionSummary,
- widget: QWidget,
- *,
- action_kind: WidgetActionKind,
- ) -> UiWidgetActionInvokeResult:
- try:
- DEFAULT_WIDGET_DESCRIPTOR_PROJECTOR_REGISTRY.projector_for(
- widget
- ).invoke_action(
- widget,
- action_kind,
- target_index=request.target_index,
- )
- except WidgetActionUnsupportedError:
- return self.error(
- request,
- AgentError(
- code=UiWidgetActionIssueCode.ACTION_UNSUPPORTED.value,
- message=(
- f"Widget path_id {request.path_id!r} does not support "
- f"action kind {action_kind.value!r}."
- ),
- ),
- summary=action_summary,
- action_kind=action_kind.value,
- )
- except WidgetActionTargetInvalidError as error:
- return self.error(
- request,
- AgentError(
- code=UiWidgetActionIssueCode.INDEX_INVALID.value,
- message=(
- f"Target index {request.target_index!r} is outside the "
- f"available range for widget path_id {request.path_id!r}."
- ),
- hint=(
- "Refresh the widget tree and choose target_index from 0 "
- f"through {error.item_count - 1}."
- ),
- ),
- summary=action_summary,
- action_kind=action_kind.value,
- )
-
- return UiWidgetActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- path_id=request.path_id,
- action_kind=action_kind.value,
- invoked=True,
- receipt=UiMutationReceipt.accepted_for(request.request_token),
- summary=action_summary,
- )
-
- def _invoke_item_action(
- self,
- request: UiWidgetActionInvokeRequest,
- target: WindowProjectionTarget,
- path: tuple[int, ...],
- descriptor: WidgetDescriptor,
- action_summary: UiWidgetActionSummary,
- *,
- action_kind: WidgetActionKind,
- ) -> UiWidgetActionInvokeResult:
- guard_error = self._descriptor_guard_error(request, descriptor, action_kind)
- if guard_error is not None:
- return self.error(
- request,
- guard_error,
- summary=action_summary,
- action_kind=action_kind.value,
- )
-
- resolution = UiWidgetPathResolver.item_view_index_at_path(target.widget, path)
- if resolution is None:
- return self.error(
- request,
- AgentError(
- code=UiWidgetActionIssueCode.ACTION_UNSUPPORTED.value,
- message=(
- f"Widget path_id {request.path_id!r} does not resolve to "
- "an actionable item-view row."
- ),
- ),
- summary=action_summary,
- action_kind=action_kind.value,
- )
-
- action_kind.item_action.defer(resolution.view, resolution.index)
- return UiWidgetActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- path_id=request.path_id,
- action_kind=action_kind.value,
- invoked=True,
- receipt=UiMutationReceipt.accepted_for(request.request_token),
- summary=action_summary,
- )
-
- def _resolve_action_kind(
- self,
- request: UiWidgetActionInvokeRequest,
- descriptor: WidgetDescriptor,
- ) -> WidgetActionKind | AgentError:
- if request.action_kind == self.AUTO_ACTION_KIND:
- if len(descriptor.action_kinds) == 1:
- return next(iter(descriptor.action_kinds))
- default_actions = tuple(
- kind
- for kind in descriptor.action_kinds
- if kind.item_action is not None and kind.item_action.default
- )
- if len(default_actions) == 1:
- return default_actions[0]
- return self._action_kind_unavailable_error(request, descriptor)
- try:
- action_kind = WidgetActionKind(request.action_kind)
- except ValueError:
- return self._action_kind_unavailable_error(request, descriptor)
- if action_kind not in descriptor.action_kinds:
- return self._action_kind_unavailable_error(request, descriptor)
- return action_kind
-
- def _guard_error(
- self,
- request: UiWidgetActionInvokeRequest,
- descriptor: WidgetDescriptor,
- widget: QWidget,
- action_kind: WidgetActionKind,
- ) -> AgentError | None:
- if not descriptor.visible or not widget.isVisible():
- return AgentError(
- code=UiWidgetActionIssueCode.NOT_VISIBLE.value,
- message=f"Widget path_id {request.path_id!r} is not visible.",
- )
- if not descriptor.enabled or not widget.isEnabled():
- return AgentError(
- code=UiWidgetActionIssueCode.DISABLED.value,
- message=f"Widget path_id {request.path_id!r} is disabled.",
- )
- if not descriptor.clickable:
- return AgentError(
- code=UiWidgetActionIssueCode.NOT_CLICKABLE.value,
- message=f"Widget path_id {request.path_id!r} is not clickable.",
- )
- return None
-
- def _descriptor_guard_error(
- self,
- request: UiWidgetActionInvokeRequest,
- descriptor: WidgetDescriptor,
- action_kind: WidgetActionKind,
- ) -> AgentError | None:
- if not descriptor.visible:
- return AgentError(
- code=UiWidgetActionIssueCode.NOT_VISIBLE.value,
- message=f"Widget path_id {request.path_id!r} is not visible.",
- )
- if not descriptor.enabled:
- return AgentError(
- code=UiWidgetActionIssueCode.DISABLED.value,
- message=f"Widget path_id {request.path_id!r} is disabled.",
- )
- if not descriptor.clickable:
- return AgentError(
- code=UiWidgetActionIssueCode.NOT_CLICKABLE.value,
- message=f"Widget path_id {request.path_id!r} is not clickable.",
- )
- return None
-
- @staticmethod
- def _action_kind_unavailable_error(
- request: UiWidgetActionInvokeRequest,
- descriptor: WidgetDescriptor,
- ) -> AgentError:
- action_kinds = tuple(kind.value for kind in descriptor.action_kinds)
- return AgentError(
- code=UiWidgetActionIssueCode.ACTION_KIND_UNAVAILABLE.value,
- message=(
- f"Widget path_id {request.path_id!r} does not expose "
- f"action kind {request.action_kind!r}."
- ),
- hint=f"Available action kinds: {', '.join(action_kinds) or 'none'}.",
- )
-
- @staticmethod
- def error(
- request: UiWidgetActionInvokeRequest,
- error: AgentError,
- *,
- summary: UiWindowSummary | UiWidgetActionSummary | None = None,
- action_kind: str | None = None,
- ) -> UiWidgetActionInvokeResult:
- action_summary = summary if isinstance(summary, UiWidgetActionSummary) else None
- return UiWidgetActionInvokeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- path_id=request.path_id,
- action_kind=action_kind or request.action_kind,
- invoked=False,
- receipt=UiMutationReceipt.rejected_for(request.request_token),
- summary=action_summary,
- errors=(error,),
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _WidgetGeometryKey:
- """Stable enough bridge key for matching live form widgets to descriptors."""
-
- x: int
- y: int
- width: int
- height: int
-
- @classmethod
- def from_qwidget(cls, widget: QWidget) -> "_WidgetGeometryKey":
- rect = widget.rect()
- top_left = widget.mapToGlobal(rect.topLeft())
- return cls(
- x=top_left.x(),
- y=top_left.y(),
- width=rect.width(),
- height=rect.height(),
- )
-
- @classmethod
- def from_descriptor(cls, descriptor: WidgetDescriptor) -> "_WidgetGeometryKey":
- rect = descriptor.global_geometry
- return cls(
- x=rect.x,
- y=rect.y,
- width=rect.width,
- height=rect.height,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _WidgetFieldSemanticRecord:
- """Field semantics for one live widget action."""
-
- action_role: str
- projection: ObjectStateFieldSemanticProjection
-
- def to_action_carrier(self, *, window_id: str) -> UiWidgetActionSemanticCarrier:
- return project_dataclass(
- UiWidgetActionSemanticCarrier,
- self.projection.to_semantic_carrier(include_values=True),
- action_role=self.action_role,
- semantic_address=self.projection.semantic_address(window_id=window_id),
- object_state_scope_id=self.projection.agent_scope_id,
- field_path=self.projection.field_path,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _WidgetFieldSemanticContext:
- """Form-manager-backed lookup for widget action ObjectState semantics."""
-
- window_id: str
- records_by_geometry: dict[_WidgetGeometryKey, _WidgetFieldSemanticRecord]
-
- @classmethod
- def empty(cls, window_id: str) -> "_WidgetFieldSemanticContext":
- return cls(window_id=window_id, records_by_geometry={})
-
- @classmethod
- def from_target(
- cls,
- target: WindowProjectionTarget,
- ) -> "_WidgetFieldSemanticContext":
- if not isinstance(target.widget, BaseManagedWindow):
- return cls.empty(target.summary.window_id)
-
- records: dict[_WidgetGeometryKey, _WidgetFieldSemanticRecord] = {}
- for form_manager in target.widget.form_managers():
- cls._collect_form_manager(records, form_manager)
- return cls(
- window_id=target.summary.window_id,
- records_by_geometry=records,
- )
-
- @classmethod
- def _collect_form_manager(
- cls,
- records: dict[_WidgetGeometryKey, _WidgetFieldSemanticRecord],
- form_manager: ParameterFormManager,
- ) -> None:
- state = form_manager.state
- if not isinstance(state, ObjectState):
- return
-
- for field_name, widget in form_manager.widgets.items():
- cls._add_field_widget(
- records,
- form_manager=form_manager,
- state=state,
- field_name=field_name,
- widget=widget,
- action_role=FIELD_INPUT_ACTION_ROLE,
- )
- for field_name, widget in form_manager.reset_buttons.items():
- cls._add_field_widget(
- records,
- form_manager=form_manager,
- state=state,
- field_name=field_name,
- widget=widget,
- action_role=FIELD_RESET_ACTION_ROLE,
- )
- for nested_manager in form_manager.nested_managers.values():
- cls._collect_form_manager(records, nested_manager)
-
- @classmethod
- def _add_field_widget(
- cls,
- records: dict[_WidgetGeometryKey, _WidgetFieldSemanticRecord],
- *,
- form_manager: ParameterFormManager,
- state: ObjectState,
- field_name: str,
- widget: QWidget,
- action_role: str,
- ) -> None:
- field_path = cls._field_path(form_manager, field_name)
- if field_path not in state.parameters:
- return
- records[_WidgetGeometryKey.from_qwidget(widget)] = _WidgetFieldSemanticRecord(
- action_role=action_role,
- projection=ObjectStateFieldSemanticProjection.of(
- state,
- field_path,
- ),
- )
-
- @staticmethod
- def _field_path(form_manager: ParameterFormManager, field_name: str) -> str:
- if form_manager.field_id:
- return f"{form_manager.field_id}.{field_name}"
- return field_name
-
- def for_descriptor(
- self,
- descriptor: WidgetDescriptor,
- ) -> _WidgetFieldSemanticRecord | None:
- return self.records_by_geometry.get(
- _WidgetGeometryKey.from_descriptor(descriptor)
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _WidgetItemSemanticRecord:
- """ObjectState semantics already attached to one projected item-view row."""
-
- agent_scope_id: str | None
- dirty_fields: tuple[str, ...]
- signature_diff_fields: tuple[str, ...]
-
- @classmethod
- def from_index(cls, index: QModelIndex) -> "_WidgetItemSemanticRecord":
- scope_value = index.data(OBJECT_STATE_PATH_ROLE)
- agent_scope_id = None
- if isinstance(scope_value, str):
- agent_scope_id = _agent_object_state_scope_id(scope_value)
- return cls(
- agent_scope_id=agent_scope_id,
- dirty_fields=cls._field_tuple(index.data(DIRTY_FIELDS_ROLE)),
- signature_diff_fields=cls._field_tuple(index.data(SIG_DIFF_FIELDS_ROLE)),
- )
-
- @staticmethod
- def _field_tuple(value) -> tuple[str, ...]:
- if isinstance(value, str):
- return (value,)
- if isinstance(value, (frozenset, list, set, tuple)):
- return tuple(field for field in value if isinstance(field, str))
- return ()
-
- @property
- def has_semantics(self) -> bool:
- return bool(
- self.agent_scope_id or self.dirty_fields or self.signature_diff_fields
- )
-
- @property
- def dirty(self) -> bool:
- return bool(self.dirty_fields)
-
- @property
- def signature_diff(self) -> bool:
- return bool(self.signature_diff_fields)
-
- @property
- def semantic_markers(self) -> tuple[str, ...]:
- return field_markers(dirty=self.dirty, signature_diff=self.signature_diff)
-
- def to_action_carrier(self) -> UiWidgetActionSemanticCarrier:
- return UiWidgetActionSemanticCarrier(
- action_role=ITEM_SELECT_ACTION_ROLE,
- object_state_scope_id=self.agent_scope_id,
- dirty=self.dirty,
- signature_diff=self.signature_diff,
- semantic_markers=self.semantic_markers,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _WidgetItemSemanticContext:
- """Resolve shared manager-list item roles for projected item rows."""
-
- root_widget: QWidget | None
-
- @classmethod
- def empty(cls) -> "_WidgetItemSemanticContext":
- return cls(root_widget=None)
-
- @classmethod
- def from_target(
- cls,
- target: WindowProjectionTarget,
- ) -> "_WidgetItemSemanticContext":
- return cls(root_widget=target.widget)
-
- def for_descriptor(
- self,
- descriptor: WidgetDescriptor,
- ) -> _WidgetItemSemanticRecord | None:
- if self.root_widget is None:
- return None
- resolution = UiWidgetPathResolver.item_view_index_at_path(
- self.root_widget,
- descriptor.path,
- )
- if resolution is None:
- return None
- record = _WidgetItemSemanticRecord.from_index(resolution.index)
- if not record.has_semantics:
- return None
- return record
-
-
-class UiWidgetTreeResultFactory:
- """Build UI bridge widget-tree results from resolved Qt window targets."""
-
- def project(
- self,
- request: UiWidgetTreeRequest,
- target: WindowProjectionTarget,
- ) -> UiWidgetTreeResult:
- try:
- projection = WidgetTreeProjectionService.project(
- target.widget,
- policy=request.as_projection_policy(),
- )
- except Exception as exc:
- return self.error(
- request,
- AgentError.from_exception("ui_widget_tree_projection_failed", exc),
- summary=target.summary,
- )
- return self.from_projection(
- request,
- target.summary,
- projection,
- field_semantics=_WidgetFieldSemanticContext.from_target(target),
- item_semantics=_WidgetItemSemanticContext.from_target(target),
- )
-
- @classmethod
- def from_projection(
- cls,
- request: UiWidgetTreeRequest,
- summary: UiWindowSummary,
- projection: WidgetTreeProjection,
- field_semantics: _WidgetFieldSemanticContext | None = None,
- item_semantics: _WidgetItemSemanticContext | None = None,
- ) -> UiWidgetTreeResult:
- if field_semantics is None:
- field_semantics = _WidgetFieldSemanticContext.empty(summary.window_id)
- if item_semantics is None:
- item_semantics = _WidgetItemSemanticContext.empty()
- action_draft = cls._draft_node(
- projection.root,
- actionable_only=False,
- )
- assert action_draft is not None
- draft = cls._draft_node(
- projection.root,
- actionable_only=request.actionable_only,
- )
- actionable_widgets = tuple(
- cls.action_summaries_from_draft(
- action_draft,
- field_semantics=field_semantics,
- item_semantics=item_semantics,
- )
- )
- root = None
- returned_widget_count = 0
- if request.include_tree and draft is not None:
- root = cls.node_from_draft(draft)
- returned_widget_count = cls.included_node_count(draft)
- return UiWidgetTreeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- projected=True,
- root=root,
- actionable_widgets=actionable_widgets,
- summary=summary,
- widget_count=projection.widget_count,
- actionable_count=len(actionable_widgets),
- returned_widget_count=returned_widget_count,
- returned_actionable_count=len(actionable_widgets),
- tree_truncated=projection.truncated,
- actionable_widgets_truncated=projection.truncated,
- actionable_only=request.actionable_only,
- include_tree=request.include_tree,
- max_depth=request.max_depth,
- max_nodes=request.max_nodes,
- )
-
- @classmethod
- def _draft_node(
- cls,
- descriptor: WidgetDescriptor,
- *,
- actionable_only: bool,
- ) -> "_WidgetTreeNodeDraft | None":
- children: list[_WidgetTreeNodeDraft] = []
- for child in descriptor.children:
- child_draft = cls._draft_node(child, actionable_only=actionable_only)
- if child_draft is not None:
- children.append(child_draft)
-
- if (
- descriptor.path
- and actionable_only
- and not descriptor.actionable
- and not children
- ):
- return None
- return _WidgetTreeNodeDraft(descriptor, tuple(children))
-
- @classmethod
- def node_from_draft(
- cls,
- draft: "_WidgetTreeNodeDraft",
- ) -> UiWidgetTreeNode:
- return cls.node(
- draft.descriptor,
- children=tuple(cls.node_from_draft(child) for child in draft.children),
- )
-
- @classmethod
- def included_node_count(cls, draft: "_WidgetTreeNodeDraft") -> int:
- return 1 + sum(cls.included_node_count(child) for child in draft.children)
-
- @classmethod
- def action_summaries_from_draft(
- cls,
- draft: "_WidgetTreeNodeDraft",
- *,
- field_semantics: _WidgetFieldSemanticContext,
- item_semantics: _WidgetItemSemanticContext,
- ancestors: tuple["_WidgetTreeNodeDraft", ...] = (),
- ):
- if draft.descriptor.actionable:
- summary = cls.action_summary(
- draft.descriptor,
- context_label=cls.action_context_label(draft, ancestors),
- field_semantics=field_semantics,
- item_semantics=item_semantics,
- )
- if UiWidgetActionSummaryPolicy.includes(summary):
- yield summary
-
- for child in draft.children:
- yield from cls.action_summaries_from_draft(
- child,
- field_semantics=field_semantics,
- item_semantics=item_semantics,
- ancestors=(*ancestors, draft),
- )
-
- @classmethod
- def action_summary(
- cls,
- descriptor: WidgetDescriptor,
- *,
- context_label: str | None = None,
- field_semantics: _WidgetFieldSemanticContext | None = None,
- item_semantics: _WidgetItemSemanticContext | None = None,
- ) -> UiWidgetActionSummary:
- label = cls.action_label(descriptor)
- action_kinds = tuple(kind.value for kind in descriptor.action_kinds)
- summary = project_dataclass(
- UiWidgetActionSummary,
- descriptor,
- label=label,
- geometry=cls.rect(descriptor.geometry),
- global_geometry=cls.rect(descriptor.global_geometry),
- action_kinds=action_kinds,
- context_label=context_label,
- )
- return overlay_non_none_dataclass(
- summary,
- cls.action_semantic_carrier(
- descriptor,
- label=label,
- action_kinds=action_kinds,
- field_semantics=field_semantics,
- item_semantics=item_semantics,
- ),
- )
-
- @staticmethod
- def action_semantic_carrier(
- descriptor: WidgetDescriptor,
- *,
- label: str | None,
- action_kinds: tuple[str, ...],
- field_semantics: _WidgetFieldSemanticContext | None,
- item_semantics: _WidgetItemSemanticContext | None,
- ) -> UiWidgetActionSemanticCarrier:
- if field_semantics is not None:
- field_record = field_semantics.for_descriptor(descriptor)
- if field_record is not None:
- return field_record.to_action_carrier(
- window_id=field_semantics.window_id
- )
- if item_semantics is not None:
- item_record = item_semantics.for_descriptor(descriptor)
- if item_record is not None:
- return item_record.to_action_carrier()
- if FieldResetWidgetActionSummary.matches_fields(
- class_name=descriptor.class_name,
- label=label,
- object_name=descriptor.object_name,
- action_kinds=action_kinds,
- ):
- return UiWidgetActionSemanticCarrier(action_role=FIELD_RESET_ACTION_ROLE)
- return UiWidgetActionSemanticCarrier()
-
- @classmethod
- def action_context_label(
- cls,
- draft: "_WidgetTreeNodeDraft",
- ancestors: tuple["_WidgetTreeNodeDraft", ...],
- ) -> str | None:
- branch = draft
- for ancestor in reversed(ancestors):
- siblings = tuple(
- child
- for child in ancestor.children
- if child.descriptor.path != branch.descriptor.path
- )
- if siblings:
- label = cls._first_context_label(siblings)
- if label:
- return label
- branch = ancestor
- return None
-
- @classmethod
- def _first_context_label(
- cls,
- drafts: tuple["_WidgetTreeNodeDraft", ...],
- ) -> str | None:
- for draft in drafts:
- descriptor = draft.descriptor
- if not descriptor.actionable and not descriptor.action_kinds:
- label = cls._descriptor_context_label(descriptor)
- if label:
- return label
- child_label = cls._first_context_label(draft.children)
- if child_label:
- return child_label
- return None
-
- @staticmethod
- def _descriptor_context_label(descriptor: WidgetDescriptor) -> str | None:
- for label in (
- descriptor.text,
- descriptor.title,
- descriptor.accessible_name,
- ):
- if label:
- return label
- return None
-
- @staticmethod
- def action_label(descriptor: WidgetDescriptor) -> str | None:
- for label in (
- descriptor.text,
- descriptor.title,
- descriptor.accessible_name,
- descriptor.current_text,
- descriptor.object_name,
- ):
- if label:
- return label
- return None
-
- @classmethod
- def node(
- cls,
- descriptor: WidgetDescriptor,
- *,
- children: tuple[UiWidgetTreeNode, ...] | None = None,
- ) -> UiWidgetTreeNode:
- return UiWidgetTreeNode(
- path=descriptor.path,
- path_id=descriptor.path_id,
- child_index=descriptor.child_index,
- class_name=descriptor.class_name,
- object_name=descriptor.object_name,
- visible=descriptor.visible,
- enabled=descriptor.enabled,
- geometry=cls.rect(descriptor.geometry),
- global_geometry=cls.rect(descriptor.global_geometry),
- tool_tip=descriptor.tool_tip,
- status_tip=descriptor.status_tip,
- whats_this=descriptor.whats_this,
- window_title=descriptor.window_title,
- accessible_name=descriptor.accessible_name,
- accessible_description=descriptor.accessible_description,
- text=descriptor.text,
- text_truncated=descriptor.text_truncated,
- title=descriptor.title,
- action_kinds=tuple(kind.value for kind in descriptor.action_kinds),
- clickable=descriptor.clickable,
- actionable=descriptor.actionable,
- checkable=descriptor.checkable,
- checked=descriptor.checked,
- current_index=descriptor.current_index,
- current_text=descriptor.current_text,
- item_count=descriptor.item_count,
- item_texts=descriptor.item_texts,
- children=(
- tuple(cls.node(child) for child in descriptor.children)
- if children is None
- else children
- ),
- )
-
- @staticmethod
- def rect(rect: WidgetRect) -> UiWidgetRect:
- return UiWidgetRect(
- x=rect.x,
- y=rect.y,
- width=rect.width,
- height=rect.height,
- )
-
- @staticmethod
- def error(
- request: UiWidgetTreeRequest,
- error: AgentError,
- *,
- summary: UiWindowSummary | None = None,
- ) -> UiWidgetTreeResult:
- return UiWidgetTreeResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- projected=False,
- summary=summary,
- errors=(error,),
- actionable_only=request.actionable_only,
- include_tree=request.include_tree,
- max_depth=request.max_depth,
- max_nodes=request.max_nodes,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class _WidgetTreeNodeDraft:
- descriptor: WidgetDescriptor
- children: tuple["_WidgetTreeNodeDraft", ...]
-
-
-class FieldResetWidgetActionSummary:
- """Action-summary rule for per-field reset buttons."""
-
- BUTTON_CLASS_NAME = "QPushButton"
- LABEL = PARAMETER_FORM_CONSTANTS.RESET_BUTTON_TEXT
- OBJECT_NAME_SUFFIX = (
- f"{PARAMETER_FORM_CONSTANTS.FIELD_ID_SEPARATOR}"
- f"{PARAMETER_FORM_CONSTANTS.RESET_BUTTON_TEXT.lower()}"
- )
-
- @classmethod
- def matches(cls, summary: UiWidgetActionSummary) -> bool:
- return cls.matches_fields(
- class_name=summary.class_name,
- label=summary.label,
- object_name=summary.object_name,
- action_kinds=summary.action_kinds,
- )
-
- @classmethod
- def matches_fields(
- cls,
- *,
- class_name: str,
- label: str | None,
- object_name: str,
- action_kinds: tuple[str, ...],
- ) -> bool:
- return (
- class_name == cls.BUTTON_CLASS_NAME
- and label == cls.LABEL
- and object_name.endswith(cls.OBJECT_NAME_SUFFIX)
- and action_kinds == ("button",)
- )
-
-
-class UiWidgetActionSummaryPolicy:
- """Inclusion policy for action summaries."""
-
- @staticmethod
- def includes(summary: UiWidgetActionSummary) -> bool:
- del summary
- return True
-
-
-class UiWindowProjectionService(
- ScopeWindowTargetOperationProjectionABC,
- WindowCatalogProjectionABC,
-):
- """Project the main-window/WindowManager window graph into agent DTOs."""
-
- projection_id = MAIN_WINDOW_PROVIDER_ID
- identity = UiWindowProviderIdentity(
- provider_id=MAIN_WINDOW_PROVIDER_ID,
- title="Main window windows",
- )
-
- def __init__(self, main_window: "OpenHCSMainWindow") -> None:
- self._embedded = EmbeddedWindowProjection(main_window)
- self._managed = ManagedWindowProjection(main_window)
- self._dynamic = DynamicScopeWindowProjection()
-
- def summaries(self) -> tuple[UiWindowSummary, ...]:
- route_index = self._route_index()
- return (
- tuple(route.summary() for route in route_index.embedded_routes)
- + tuple(route.summary() for route in route_index.managed_routes)
- + self._dynamic.summaries(route_index)
- )
-
- def overview_sections(self) -> tuple[UiLiveOverviewSection, ...]:
- route_index = self._route_index()
- return WindowCatalogProjectionABC.overview_sections(self) + tuple(
- section
- for route in route_index.embedded_routes
- for section in route.overview_sections()
- )
-
- def handles(self, window_id: str) -> bool:
- identity = UiWindowIdentity(window_id=window_id)
- route_index = self._route_index()
- if route_index.embedded_route(identity) is not None:
- return True
- if route_index.managed_route(identity) is not None:
- return True
- if identity.window_id in WindowManager.get_open_scopes():
- return True
- if self._open_window_manager_scope_id(identity) is not None:
- return True
- return ScopeWindowRegistry.find_handler(identity.window_id) is not None
-
- def focus(self, request: UiWindowFocusRequest) -> UiWindowFocusResult:
- identity = request.as_identity()
- route_index = self._route_index()
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if embedded_route is not None:
- return self._focused_result(request, embedded_route.focus())
-
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if managed_route is not None and request.open_policy.create_if_missing:
- return self._focused_result(request, managed_route.focus())
-
- open_scope_id = self._open_window_manager_scope_id(identity)
- if open_scope_id is not None:
- if WindowManager.focus_and_navigate(open_scope_id):
- return self._focused_result(
- request,
- self._dynamic_scope_projection().summary(
- identity,
- open_scope_id,
- ),
- )
-
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if embedded_route is not None:
- return self._focused_result(request, embedded_route.focus())
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if managed_route is not None and request.open_policy.create_if_missing:
- return self._focused_result(request, managed_route.focus())
-
- result = ScopeWindowNavigationService.navigate(
- WindowNavigationRequest(
- scope_id=identity.window_id,
- create_if_missing=request.open_policy.create_if_missing,
- )
- )
- if result.focused:
- return self._focused_result(
- request,
- self._dynamic_scope_projection().summary(
- identity,
- result.window_scope_id,
- ),
- )
-
- return UiWindowFocusResult(
- schema_version=SCHEMA_VERSION,
- window_id=identity.window_id,
- focused=False,
- errors=(WindowProjectionResultAuthority.unknown_window(identity),),
- )
-
- def _navigate_static_route(
- self,
- request: UiWindowNavigateRequest,
- resolution: WindowRouteResolution,
- completed: Callable[[UiWindowNavigateResult], None] | None = None,
- ) -> UiWindowNavigateResult:
- dispatch = resolution.route.navigate(
- request,
- self._completion_for(
- request,
- completed,
- created=False,
- summary=resolution.summary,
- ),
- )
- return self._navigate_result(
- request,
- focused=dispatch.focused,
- created=False,
- navigated=dispatch.navigated,
- summary=resolution.summary(),
- )
-
- def navigate(
- self,
- request: UiWindowNavigateRequest,
- completed: Callable[[UiWindowNavigateResult], None] | None = None,
- ) -> UiWindowNavigateResult:
- identity = request.as_identity()
- route_index = self._route_index()
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if embedded_route is not None:
- return self._navigate_static_route(request, embedded_route, completed)
-
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if managed_route is not None and request.open_policy.create_if_missing:
- return self._navigate_static_route(request, managed_route, completed)
-
- open_scope_id = self._open_window_manager_scope_id(identity)
- if open_scope_id is not None:
- dispatch = WindowManager.focus_and_navigate_result(
- open_scope_id,
- item_id=request.item_id,
- field_path=request.field_path,
- requested_scope_id=identity.window_id,
- completed=self._completion_for(
- request,
- completed,
- created=False,
- summary=lambda: self._dynamic_scope_projection().summary(
- identity, open_scope_id
- ),
- ),
- )
- if dispatch.focused:
- return self._navigate_result(
- request,
- focused=True,
- created=False,
- navigated=dispatch.navigated,
- summary=self._dynamic_scope_projection().summary(
- identity,
- open_scope_id,
- ),
- )
-
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if embedded_route is not None:
- return self._navigate_static_route(request, embedded_route, completed)
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if managed_route is not None and request.open_policy.create_if_missing:
- return self._navigate_static_route(request, managed_route, completed)
-
- result = ScopeWindowNavigationService.navigate(
- WindowNavigationRequest(
- scope_id=identity.window_id,
- item_id=request.item_id,
- field_path=request.field_path,
- create_if_missing=request.open_policy.create_if_missing,
- ),
- completed=self._completion_for(
- request,
- completed,
- created=True,
- summary=lambda: self._dynamic_scope_projection().summary(
- identity, identity.window_id
- ),
- ),
- )
- if result.focused:
- return self._navigate_result(
- request,
- focused=True,
- created=result.created,
- navigated=result.navigated,
- summary=self._dynamic_scope_projection().summary(
- identity,
- result.window_scope_id,
- ),
- )
-
- return UiWindowNavigateResult(
- schema_version=SCHEMA_VERSION,
- window_id=identity.window_id,
- focused=False,
- navigated=False,
- created=result.created,
- errors=(WindowProjectionResultAuthority.unknown_window(identity),),
- )
-
- def close(self, request: UiWindowCloseRequest) -> UiWindowCloseResult:
- identity = request.as_identity()
- route_index = self._route_index()
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if embedded_route is not None:
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "ui_window_close_unsupported",
- "Embedded main-window panes cannot be closed through this UI bridge operation.",
- summary=embedded_route.summary(),
- )
-
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=False,
- )
- if managed_route is not None:
- return self._close_window_manager_scope(
- request,
- managed_route.summary(),
- )
-
- open_scope_id = self._open_window_manager_scope_id(identity)
- if open_scope_id is not None:
- return self._close_window_manager_scope(
- request,
- self._dynamic_scope_projection().summary(
- identity,
- open_scope_id,
- ),
- )
-
- embedded_route = self._embedded_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if embedded_route is not None:
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "ui_window_close_unsupported",
- "Embedded main-window panes cannot be closed through this UI bridge operation.",
- summary=embedded_route.summary(),
- )
- managed_route = self._managed_route_resolution(
- identity,
- route_index,
- resolve_scope_alias=True,
- )
- if managed_route is not None:
- return self._close_window_manager_scope(
- request,
- managed_route.summary(),
- )
-
- scope = UiWindowManagerScope.from_identity(
- self._resolved_window_identity(identity)
- )
- scope_widget = WindowManager.get_window(scope.value)
- if scope_widget is not None:
- return self._close_window_manager_scope(
- request,
- self._dynamic_scope_projection().summary(
- identity,
- scope.value,
- ),
- )
-
- if ScopeWindowRegistry.find_handler(identity.window_id) is not None:
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "ui_window_not_open",
- f"UI window scope is not currently open: {identity.window_id!r}",
- )
-
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "unknown_ui_window",
- f"Unknown or closed UI window: {identity.window_id!r}",
- )
-
- @staticmethod
- def _close_window_manager_scope(
- request: UiWindowCloseRequest,
- summary: UiWindowSummary,
- ) -> UiWindowCloseResult:
- scope = summary.manager_scope
- if scope is None:
- scope = UiWindowManagerScope.from_identity(request.as_identity())
- if WindowManager.close_window(scope.value):
- return WindowCloseResultBoundaryPolicy.closed(
- request,
- summary,
- )
- return WindowCloseResultBoundaryPolicy.error(
- request,
- "ui_window_close_rejected",
- f"WindowManager rejected the close request for scope: {scope.value!r}",
- summary=summary,
- )
-
- def _route_index(self) -> WindowRouteIndex:
- embedded_routes = self._embedded.routes()
- return WindowRouteIndex(
- embedded_routes=embedded_routes,
- managed_routes=self._managed.routes(embedded_routes),
- )
-
- def _dynamic_scope_projection(self) -> DynamicScopeWindowProjection:
- return self._dynamic
-
- @staticmethod
- def _focused_result(
- request: UiWindowFocusRequest,
- summary: UiWindowSummary,
- ) -> UiWindowFocusResult:
- return UiWindowFocusResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=True,
- summary=summary,
- )
-
- @staticmethod
- def _completion_for(
- request: UiWindowNavigateRequest,
- completed: Callable[[UiWindowNavigateResult], None] | None,
- *,
- created: bool,
- summary: Callable[[], UiWindowSummary],
- ) -> Callable[[RegisteredWindowNavigationCompletion], None] | None:
- if completed is None:
- return None
-
- def finish(facts: RegisteredWindowNavigationCompletion) -> None:
- errors = ()
- if facts.error is not None:
- errors = (
- AgentError.from_exception(
- UiBridgeNavigateWindowOperation.failure_error_code, facts.error
- ),
- )
- elif not facts.executed:
- errors = (
- AgentError(
- code=UiBridgeNavigateWindowOperation.failure_error_code,
- message=(
- "Window was destroyed before navigation."
- if not facts.window_alive
- else f"Navigation readiness exhausted: {facts.wait_reason}."
- ),
- ),
- )
- completed(
- UiWindowNavigateResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=facts.window_alive,
- created=created,
- navigated=facts.executed,
- target_exposed=facts.target_exposed,
- summary=summary() if facts.window_alive else None,
- errors=errors,
- )
- )
-
- return finish
-
- @staticmethod
- def _navigate_result(
- request: UiWindowNavigateRequest,
- *,
- focused: bool,
- created: bool,
- navigated: bool,
- summary: UiWindowSummary,
- ) -> UiWindowNavigateResult:
- return UiWindowNavigateResult(
- schema_version=SCHEMA_VERSION,
- window_id=request.window_id,
- focused=focused,
- created=created,
- navigated=navigated,
- summary=summary,
- errors=(
- ()
- if navigated or not request.has_target
- else (
- WindowProjectionResultAuthority.unsupported_navigation_target(
- request
- ),
- )
- ),
- )
-
-
-class ManagedWindowActionProvider(UiActionProviderABC):
- """Action provider for generic WindowManager-managed form windows."""
-
- identity = MANAGED_WINDOW_ACTION_PROVIDER_IDENTITY
- dispatch_error_code = "managed_window_action_failed"
-
- def action_ids(self) -> tuple[str, ...]:
- return tuple(action.value for action in ManagedWindowAction)
-
- def summary(self, action_id: str) -> UiActionSummary:
- action = self._action(action_id)
- target_scope_ids = self._target_scope_ids(action)
- return UiActionSummary(
- schema_version=SCHEMA_VERSION,
- identity=UiActionIdentity(
- widget_id=self.identity.widget_id,
- action_id=action.value,
- ),
- title=action.title,
- enabled=bool(target_scope_ids),
- invocation_mode="async",
- side_effects=action.side_effects,
- confirmation_required=action.confirmation_required,
- selection_mode="targeted",
- required_target_count=1,
- current_selection_count=len(target_scope_ids),
- target_scope_ids=target_scope_ids,
- )
-
- def dispatch(self, request: UiActionInvokeRequest) -> UiActionDispatch:
- action = self._action(request.action_id)
- (target_scope_id,) = request.selected_scope_ids
- action.dispatch(self._target_window(target_scope_id, action))
- return UiActionDispatch()
-
- @staticmethod
- def _action(action_id: str) -> ManagedWindowAction:
- return ManagedWindowAction(action_id)
-
- @staticmethod
- def _target_window(
- scope_id: str,
- action: ManagedWindowAction,
- ) -> BaseFormDialog:
- window = WindowManager.get_window(scope_id)
- if not isinstance(window, BaseFormDialog):
- raise ValueError(f"Window scope is not a managed form window: {scope_id!r}")
- capabilities = window.managed_window_action_capabilities()
- if not action.is_supported(capabilities):
- raise ValueError(f"Window does not support {action.value!r}: {scope_id!r}")
- return window
-
- @classmethod
- def _target_scope_ids(
- cls,
- action: ManagedWindowAction,
- ) -> tuple[str, ...]:
- return tuple(
- scope_id
- for scope_id in WindowManager.get_open_scopes()
- if cls._window_supports_action(WindowManager.get_window(scope_id), action)
- )
-
- @staticmethod
- def _window_supports_action(
- window: QWidget | None,
- action: ManagedWindowAction,
- ) -> bool:
- if not isinstance(window, BaseFormDialog):
- return False
- return action.is_supported(window.managed_window_action_capabilities())
-
-
-
@dataclass(frozen=True, slots=True)
class MainWindowBridgeProviderSet(UiBridgeProviderSetABC):
"""Provider set for main-window generic UI projections."""
diff --git a/openhcs/pyqt_gui/services/ui_window_ids.py b/openhcs/pyqt_gui/services/ui_window_ids.py
index 59a60a848e..e6c69969ef 100644
--- a/openhcs/pyqt_gui/services/ui_window_ids.py
+++ b/openhcs/pyqt_gui/services/ui_window_ids.py
@@ -2,8 +2,6 @@
from __future__ import annotations
-from collections.abc import Mapping
-from types import MappingProxyType
from typing import ClassVar
from openhcs.agent.ui_bridge_identities import (
@@ -14,7 +12,7 @@
LogViewerWindowIdentity,
MainWindowWidgetIdentity,
PipelineEditorWidgetIdentity,
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
SystemMonitorWindowIdentity,
ZmqServerManagerWindowIdentity,
)
@@ -24,7 +22,7 @@ class OpenHCSUiWindowId:
"""Closed identifiers for stable windows exposed through the UI bridge."""
main_window: ClassVar[str] = MainWindowWidgetIdentity.require_value()
- plate_manager: ClassVar[str] = PlateManagerWidgetIdentity.require_value()
+ plate_manager: ClassVar[str] = DatasetListWidgetIdentity.require_value()
pipeline_editor: ClassVar[str] = PipelineEditorWidgetIdentity.require_value()
system_monitor: ClassVar[str] = SystemMonitorWindowIdentity.require_value()
zmq_server_manager: ClassVar[str] = ZmqServerManagerWindowIdentity.require_value()
@@ -33,34 +31,3 @@ class OpenHCSUiWindowId:
global_config: ClassVar[str] = GlobalConfigWindowIdentity.require_value()
knowledge_base: ClassVar[str] = KnowledgeBaseWindowIdentity.require_value()
about: ClassVar[str] = AboutOpenHCSWindowIdentity.require_value()
-
- manager_scope_aliases: ClassVar[Mapping[str, str]] = MappingProxyType(
- {
- "": global_config,
- }
- )
-
- @classmethod
- def agent_window_id_for_manager_scope(cls, manager_scope_id: str) -> str:
- if manager_scope_id in cls.manager_scope_aliases:
- return cls.manager_scope_aliases[manager_scope_id]
- return manager_scope_id
-
- @classmethod
- def manager_scopes_for_agent_window_id(
- cls, agent_window_id: str
- ) -> tuple[str, ...]:
- aliased_scopes = tuple(
- manager_scope_id
- for manager_scope_id, aliased_window_id in cls.manager_scope_aliases.items()
- if aliased_window_id == agent_window_id
- and manager_scope_id != agent_window_id
- )
- return (agent_window_id,) + aliased_scopes
-
- @classmethod
- def canonical_manager_scope_for_agent_window_id(cls, agent_window_id: str) -> str:
- for manager_scope_id, aliased_window_id in cls.manager_scope_aliases.items():
- if aliased_window_id == agent_window_id:
- return manager_scope_id
- return agent_window_id
diff --git a/openhcs/pyqt_gui/services/window_handlers.py b/openhcs/pyqt_gui/services/window_handlers.py
index 09491d9c48..b41575a7ae 100644
--- a/openhcs/pyqt_gui/services/window_handlers.py
+++ b/openhcs/pyqt_gui/services/window_handlers.py
@@ -19,6 +19,7 @@
from openhcs.authoring.session.step_scopes import StepEditorScope
from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
from openhcs.ui.shared.plate_scope_identity import PipelineScopeIdentity
+from formbridge.bridge.identities import UiStableWindowIdentityDeclaration
if TYPE_CHECKING:
from objectstate.object_state import ObjectState
@@ -131,7 +132,7 @@ def handle_ui_save(new_config: UIConfig) -> None:
),
),
),
- scope_id=OpenHCSUiWindowId.canonical_manager_scope_for_agent_window_id(
+ scope_id=UiStableWindowIdentityDeclaration.manager_scope_for_window_id(
global_config_window_scope_id(request.scope_id)
),
title_text=GlobalConfigWindowIdentity.require_title(),
@@ -355,7 +356,7 @@ def _select_step_editor_tab(
window.tab_widget.setCurrentIndex(tab_index)
def _plate_manager(self):
- from pyqt_reactive.services.service_registry import ServiceRegistry
+ from formbridge.services.service_registry import ServiceRegistry
from openhcs.pyqt_gui.widgets.plate_manager import PlateManagerWidget
diff --git a/openhcs/pyqt_gui/session_rendering.py b/openhcs/pyqt_gui/session_rendering.py
index 76b7be501c..0a7466690c 100644
--- a/openhcs/pyqt_gui/session_rendering.py
+++ b/openhcs/pyqt_gui/session_rendering.py
@@ -18,7 +18,7 @@
from metaclass_registry import AutoRegisterMeta
from PyQt6.QtCore import QObject, pyqtSignal
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from openhcs.agent.dto.session import SessionOperationResult
from openhcs.authoring.session.operations import SessionOperation
from openhcs.authoring.session.operations.datasets import PromptedOperation
diff --git a/openhcs/pyqt_gui/ui_tab_identities.py b/openhcs/pyqt_gui/ui_tab_identities.py
index 6d6a772a10..6d124842ef 100644
--- a/openhcs/pyqt_gui/ui_tab_identities.py
+++ b/openhcs/pyqt_gui/ui_tab_identities.py
@@ -5,7 +5,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Self
-from pyqt_reactive.services.tab_identity import TabLabelDeclarationMixin
+from formbridge.services.tab_identity import TabLabelDeclarationMixin
if TYPE_CHECKING:
from pyqt_reactive.widgets.shared import ActionTabbedWindowBody
diff --git a/openhcs/pyqt_gui/widgets/config_preview_formatters.py b/openhcs/pyqt_gui/widgets/config_preview_formatters.py
index 70c6987cfd..25850e418f 100644
--- a/openhcs/pyqt_gui/widgets/config_preview_formatters.py
+++ b/openhcs/pyqt_gui/widgets/config_preview_formatters.py
@@ -2,10 +2,7 @@
from typing import TYPE_CHECKING
-from pyqt_reactive.utils.preview_formatters import (
- check_enabled_field,
- resolve_preview_label,
-)
+from formbridge.utils.preview_formatters import check_enabled_field, resolve_preview_label
if TYPE_CHECKING:
from openhcs.core.config import PartitionFilterConfig
diff --git a/openhcs/pyqt_gui/widgets/debug_toolbar.py b/openhcs/pyqt_gui/widgets/debug_toolbar.py
index 593422391b..9a28cd30cd 100644
--- a/openhcs/pyqt_gui/widgets/debug_toolbar.py
+++ b/openhcs/pyqt_gui/widgets/debug_toolbar.py
@@ -11,9 +11,7 @@
)
from openhcs.core.debug import DebugCommandType
from openhcs.core.execution_state import ManagerExecutionState
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- UiOwnedStateSurfaceDeclaration,
-)
+from formbridge.bridge.contracts import UiOwnedStateSurfaceDeclaration
from openhcs.pyqt_gui.widgets.shared.services.debug_session_projection import (
DebugActionRenderModel,
DebugSessionPanelText,
diff --git a/openhcs/pyqt_gui/widgets/pipeline_editor.py b/openhcs/pyqt_gui/widgets/pipeline_editor.py
index cb99806d97..be6505e476 100644
--- a/openhcs/pyqt_gui/widgets/pipeline_editor.py
+++ b/openhcs/pyqt_gui/widgets/pipeline_editor.py
@@ -5,7 +5,7 @@
them on every pipeline event; every change goes through the session.
"""
-from pyqt_reactive.services.window_code_document import use_external_code_editor
+from formbridge.services.window_code_document import use_external_code_editor
from objectstate import ScopeKey
import copy
import logging
@@ -17,7 +17,7 @@
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QSplitter, QVBoxLayout
from pyqt_reactive.animation import WindowFlashOverlay
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.scope_token_service import ScopeTokenService
from pyqt_reactive.theming import ColorScheme
from pyqt_reactive.widgets.editors.simple_code_editor import SimpleCodeEditorService
from pyqt_reactive.widgets.shared.abstract_manager_widget import (
@@ -30,14 +30,11 @@
ListItemLeadingMarker,
)
from pyqt_reactive.widgets.shared.manager_action_controller import CodeEditorPayload
-from pyqt_reactive.widgets.shared.manager_item_hooks import (
- AttributeItemIdProjection,
- ManagerItemHooks,
-)
+from formbridge.managers.item_hooks import AttributeItemIdProjection, ManagerItemHooks
from pyqt_reactive.widgets.shared.manager_selection_controller import (
ItemSelectionPayloadProjection,
)
-from pyqt_reactive.widgets.shared.manager_state_binding import ManagerStateBinding
+from formbridge.managers.state_binding import ManagerStateBinding
from pyqt_reactive.widgets.shared.manager_ui_scaffold import (
create_manager_header,
create_manager_list_widget,
@@ -87,7 +84,7 @@
from openhcs.pyqt_gui.services.embedded_code_documents import (
EmbeddedCodeDocumentRegistrationABC,
)
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
+from formbridge.bridge.contracts import (
UiOwnedStateSurfaceDeclaration,
state_surface_declaration_for_identity,
)
diff --git a/openhcs/pyqt_gui/widgets/plate_manager.py b/openhcs/pyqt_gui/widgets/plate_manager.py
index 665a69a182..0c5076e0b8 100644
--- a/openhcs/pyqt_gui/widgets/plate_manager.py
+++ b/openhcs/pyqt_gui/widgets/plate_manager.py
@@ -7,7 +7,7 @@
from __future__ import annotations
-from pyqt_reactive.services.window_code_document import use_external_code_editor
+from formbridge.services.window_code_document import use_external_code_editor
import logging
from dataclasses import fields, replace
from functools import singledispatchmethod
@@ -17,31 +17,28 @@
from objectstate import DataclassFieldAccess
from objectstate.object_state import ObjectState, ObjectStateRegistry
from PyQt6.QtCore import pyqtSignal
-from pyqt_reactive.core.path_cache import PathCacheKey
+from formbridge.core.path_cache import PathCacheKey
from pyqt_reactive.theming import ColorScheme
from pyqt_reactive.widgets.editors.simple_code_editor import SimpleCodeEditorService
from pyqt_reactive.widgets.shared.abstract_manager_widget import (
AbstractManagerWidget,
ListItemFormat,
)
-from pyqt_reactive.widgets.shared.manager_item_hooks import (
- AttributeItemIdProjection,
- ManagerItemHooks,
-)
+from formbridge.managers.item_hooks import AttributeItemIdProjection, ManagerItemHooks
from pyqt_reactive.widgets.shared.manager_selection_controller import (
ItemIdSelectionPayloadProjection,
)
-from pyqt_reactive.widgets.shared.manager_state_binding import ManagerStateBinding
+from formbridge.managers.state_binding import ManagerStateBinding
from pyqt_reactive.widgets.shared.scope_visual_config import ListItemType
from typing_extensions import override
-from openhcs.core.selection import SelectedItems, SelectionScope
+from formbridge.selection import SelectedItems, SelectionScope
from openhcs.agent.dto.knowledge import KnowledgeBaseDocumentTarget
from openhcs.agent.dto.session import DatasetRootsRequest
from openhcs.agent.ui_bridge_identities import (
- PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration,
- PlateManagerStateSurfaceIdentityDeclaration,
- PlateManagerWidgetIdentity,
+ DatasetLiveMeasurementsStateSurfaceIdentity,
+ DatasetListStateSurfaceIdentity,
+ DatasetListWidgetIdentity,
)
from openhcs.authoring.session.dataset_document import (
DatasetDocument,
@@ -85,9 +82,7 @@
)
from openhcs.core.config import PipelineConfig
from openhcs.constants.constants import OrchestratorCreated
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- UiOwnedStateSurfaceDeclaration,
-)
+from formbridge.bridge.contracts import UiOwnedStateSurfaceDeclaration
from openhcs.pyqt_gui.session_rendering import (
GuiRenderer,
OperationPresenter,
@@ -124,10 +119,10 @@ class PlateManagerWidget(
"""
SESSION_VIEW = DatasetListView
- TITLE = PlateManagerWidgetIdentity.require_title()
+ TITLE = DatasetListWidgetIdentity.require_title()
UI_STATE_SURFACE_DECLARATIONS = (
UiOwnedStateSurfaceDeclaration(
- identity=PlateManagerStateSurfaceIdentityDeclaration,
+ identity=DatasetListStateSurfaceIdentity,
title="Plate manager state",
payload_schema="openhcs.ui.plate_manager_state.v1",
related_action_ids=tuple(
@@ -135,13 +130,13 @@ class PlateManagerWidget(
),
),
UiOwnedStateSurfaceDeclaration(
- identity=PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration,
+ identity=DatasetLiveMeasurementsStateSurfaceIdentity,
title="Live measurement results",
payload_schema="openhcs.ui.live_measurements_state.v1",
related_action_ids=(ShowLiveResults.operation_id,),
),
)
- UI_BRIDGE_WIDGET_IDENTITY = PlateManagerWidgetIdentity
+ UI_BRIDGE_WIDGET_IDENTITY = DatasetListWidgetIdentity
HELP_KNOWLEDGE_TARGET = KnowledgeBaseDocumentTarget(
document_id="openhcs_basic_interface",
section_id="plate-manager",
diff --git a/openhcs/pyqt_gui/widgets/shared/server_browser/live_tree_sync.py b/openhcs/pyqt_gui/widgets/shared/server_browser/live_tree_sync.py
index 852ce265c3..d585c4226b 100644
--- a/openhcs/pyqt_gui/widgets/shared/server_browser/live_tree_sync.py
+++ b/openhcs/pyqt_gui/widgets/shared/server_browser/live_tree_sync.py
@@ -7,9 +7,9 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTreeWidget, QTreeWidgetItem
-from pyqt_reactive.services.tree_item_key import EndpointPortProviderABC
-from pyqt_reactive.services.zmq_server_info import BaseServerInfo
-from pyqt_reactive.services.zmq_server_scan_service import StartingEndpointObservation
+from formbridge.services.tree_item_key import EndpointPortProviderABC
+from formbridge.services.zmq_server_info import BaseServerInfo
+from formbridge.services.zmq_server_scan_service import StartingEndpointObservation
from zmqruntime.viewer_state import ViewerState, ViewerStateManager
from openhcs.core.streaming_config_declarations import ViewerType
diff --git a/openhcs/pyqt_gui/widgets/shared/server_browser/presentation_models.py b/openhcs/pyqt_gui/widgets/shared/server_browser/presentation_models.py
index 049bb9b6e9..c3b3975c92 100644
--- a/openhcs/pyqt_gui/widgets/shared/server_browser/presentation_models.py
+++ b/openhcs/pyqt_gui/widgets/shared/server_browser/presentation_models.py
@@ -7,7 +7,7 @@
from typing import Callable
from PyQt6.QtWidgets import QTreeWidgetItem
-from pyqt_reactive.services.zmq_server_info import (
+from formbridge.services.zmq_server_info import (
BaseServerInfo,
ExecutionServerInfo,
GenericServerInfo,
diff --git a/openhcs/pyqt_gui/widgets/shared/server_browser/progress_projection.py b/openhcs/pyqt_gui/widgets/shared/server_browser/progress_projection.py
index 95a6bc7da6..a7e763dc43 100644
--- a/openhcs/pyqt_gui/widgets/shared/server_browser/progress_projection.py
+++ b/openhcs/pyqt_gui/widgets/shared/server_browser/progress_projection.py
@@ -7,7 +7,7 @@
from typing import Dict, List, Sequence
from PyQt6.QtWidgets import QTreeWidgetItem
-from pyqt_reactive.services.zmq_server_info import ExecutionServerInfo
+from formbridge.services.zmq_server_info import ExecutionServerInfo
from pyqt_reactive.widgets.shared import TreeStateAdapter, TreeSyncAdapter
from openhcs.core.progress import ProgressEvent
diff --git a/openhcs/pyqt_gui/widgets/shared/services/debug_session_projection.py b/openhcs/pyqt_gui/widgets/shared/services/debug_session_projection.py
index 0ca294149f..430ab201b5 100644
--- a/openhcs/pyqt_gui/widgets/shared/services/debug_session_projection.py
+++ b/openhcs/pyqt_gui/widgets/shared/services/debug_session_projection.py
@@ -92,7 +92,7 @@ def disabled_reason(
code="debug_target_required",
message="Debug controls require a selected plate.",
hint=(
- "Use plate_manager.state to select a plate, initialize it, compile it, "
+ "Use datasets.state to select a plate, initialize it, compile it, "
"then read pipeline_debug_toolbar.session."
),
)
@@ -122,7 +122,7 @@ def disabled_reason(
"session is available yet."
),
hint=(
- "Poll pipeline_debug_toolbar.session and plate_manager.state; "
+ "Poll pipeline_debug_toolbar.session and datasets.state; "
"use Stop debug session if the queued execution is stuck."
),
)
diff --git a/openhcs/pyqt_gui/widgets/shared/services/pipeline_debug_actions.py b/openhcs/pyqt_gui/widgets/shared/services/pipeline_debug_actions.py
index 6c409065c7..a1039dfdf1 100644
--- a/openhcs/pyqt_gui/widgets/shared/services/pipeline_debug_actions.py
+++ b/openhcs/pyqt_gui/widgets/shared/services/pipeline_debug_actions.py
@@ -8,7 +8,7 @@
from metaclass_registry import AutoRegisterMeta
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from python_introspect import to_jsonable
from openhcs.core.debug import DebugCommand, DebugCommandType
diff --git a/openhcs/pyqt_gui/widgets/shared/services/pipeline_editor_workflows.py b/openhcs/pyqt_gui/widgets/shared/services/pipeline_editor_workflows.py
index 34586108c2..e751bfc8bb 100644
--- a/openhcs/pyqt_gui/widgets/shared/services/pipeline_editor_workflows.py
+++ b/openhcs/pyqt_gui/widgets/shared/services/pipeline_editor_workflows.py
@@ -10,9 +10,7 @@
from objectstate.object_state import ObjectState
from PyQt6.QtWidgets import QFileDialog
-from pyqt_reactive.widgets.shared.manager_workflows import (
- ManagerCodeExecutionWorkflow,
-)
+from formbridge.managers.workflows import ManagerCodeExecutionWorkflow
from openhcs.core.callable_contract import CallableContract
from openhcs.core.debug import (
diff --git a/openhcs/pyqt_gui/widgets/shared/services/plate_manager_workflows.py b/openhcs/pyqt_gui/widgets/shared/services/plate_manager_workflows.py
index f827db6d2b..260798c892 100644
--- a/openhcs/pyqt_gui/widgets/shared/services/plate_manager_workflows.py
+++ b/openhcs/pyqt_gui/widgets/shared/services/plate_manager_workflows.py
@@ -5,7 +5,7 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
-from pyqt_reactive.widgets.shared.manager_workflows import ManagerCodeExecutionWorkflow
+from formbridge.managers.workflows import ManagerCodeExecutionWorkflow
from openhcs.authoring.session.dataset_document import (
AllDatasetDocumentScope,
diff --git a/openhcs/pyqt_gui/widgets/shared/zmq_server_manager.py b/openhcs/pyqt_gui/widgets/shared/zmq_server_manager.py
index 3f0f181d3f..e082baed45 100644
--- a/openhcs/pyqt_gui/widgets/shared/zmq_server_manager.py
+++ b/openhcs/pyqt_gui/widgets/shared/zmq_server_manager.py
@@ -6,11 +6,8 @@
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QTreeWidgetItem
-from pyqt_reactive.services.zmq_server_info import (
- BaseServerInfo,
- ExecutionServerInfo,
-)
-from pyqt_reactive.services.zmq_server_scan_service import (
+from formbridge.services.zmq_server_info import BaseServerInfo, ExecutionServerInfo
+from formbridge.services.zmq_server_scan_service import (
EndpointObservationSnapshot,
StartingEndpointObservation,
ZMQServerScanService,
@@ -26,7 +23,7 @@
from zmqruntime.progress import EventRegistryMutation
from zmqruntime.viewer_state import ViewerStateManager
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiLiveOverviewItem,
UiLiveOverviewMetric,
UiLiveOverviewSection,
@@ -35,7 +32,7 @@
from openhcs.agent.ui_bridge_identities import ZmqServerManagerWindowIdentity
from openhcs.core.progress import ProgressEvent, registry
from openhcs.pyqt_gui.config import ProgressUIConfig
-from openhcs.pyqt_gui.services.ui_bridge_contracts import UiLiveOverviewWidget
+from formbridge.bridge.contracts import UiLiveOverviewWidget
from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
from openhcs.pyqt_gui.widgets.shared.server_browser.live_tree_sync import (
LaunchingViewerServerInfo,
diff --git a/openhcs/pyqt_gui/widgets/source_bindings_editor.py b/openhcs/pyqt_gui/widgets/source_bindings_editor.py
index 4f75b5b3bc..4d7b0478fe 100644
--- a/openhcs/pyqt_gui/widgets/source_bindings_editor.py
+++ b/openhcs/pyqt_gui/widgets/source_bindings_editor.py
@@ -44,7 +44,7 @@
InlineDataclassChildFieldIdentity,
InlineDataclassFormContext,
)
-from pyqt_reactive.forms.layout_constants import CURRENT_LAYOUT
+from formbridge.forms.layout_constants import CURRENT_LAYOUT
from pyqt_reactive.forms.widget_creation_config import (
EnabledTitleWidgetMoveAuthority,
)
@@ -78,7 +78,7 @@
from pyqt_reactive.widgets.shared.clickable_help_components import (
InlineDataclassGroupBox,
)
-from pyqt_reactive.widgets.shared.scope_color_receiver import ScopeColorSchemeReceiver
+from formbridge.theming.scope_color_receiver import ScopeColorSchemeReceiver
from pyqt_reactive.widgets.shared.scoped_table_widget import ScopedTableWidget
from pyqt_reactive.widgets.structural_table import (
EditableTableSemanticBinding,
@@ -122,7 +122,7 @@
if TYPE_CHECKING:
from pyqt_reactive.forms.parameter_form_manager import ParameterFormManager
- from pyqt_reactive.forms.parameter_info_types import InlineDataclassWidgetInfo
+ from formbridge.forms.parameter_info_types import InlineDataclassWidgetInfo
SourceBindingsEditorRawValue = SourceBindingsConfig | LazyDataclass
@@ -1980,9 +1980,7 @@ def resolved_source_bindings_value(
def register_source_bindings_editor_widget() -> None:
"""Register the typed source-binding editor with pyqt-reactive forms."""
- from pyqt_reactive.forms.parameter_info_types import (
- register_inline_dataclass_widget,
- )
+ from formbridge.forms.parameter_info_types import register_inline_dataclass_widget
register_inline_dataclass_widget(
SourceBindingsConfig,
diff --git a/openhcs/pyqt_gui/widgets/step_parameter_editor.py b/openhcs/pyqt_gui/widgets/step_parameter_editor.py
index c6703d1d8e..bba67f28c1 100644
--- a/openhcs/pyqt_gui/widgets/step_parameter_editor.py
+++ b/openhcs/pyqt_gui/widgets/step_parameter_editor.py
@@ -47,19 +47,19 @@
)
from pyqt_reactive.widgets.shared.scrollable_form_mixin import ScrollableFormMixin
from pyqt_reactive.services.parameter_ops_service import ParameterOpsService
-from pyqt_reactive.services.code_editor_form_updater import CodeEditorFormUpdater
-from pyqt_reactive.services.window_code_document import (
+from formbridge.services.code_editor_form_updater import CodeEditorFormUpdater
+from formbridge.services.window_code_document import (
WindowCodeDocumentDriver,
use_external_code_editor,
)
from pyqt_reactive.widgets.editors.simple_code_editor import SimpleCodeEditorService
from pyqt_reactive.theming import ColorScheme, WidgetTheme
-from pyqt_reactive.forms.layout_constants import CURRENT_LAYOUT
+from formbridge.forms.layout_constants import CURRENT_LAYOUT
from openhcs.pyqt_gui.services.function_step_code_document import (
FunctionStepCodeDocumentDriver,
)
-from pyqt_reactive.forms.parameter_type_utils import ParameterTypeUtils
+from formbridge.forms.parameter_type_utils import ParameterTypeUtils
from objectstate.object_state import ObjectState, ObjectStateRegistry
logger = logging.getLogger(__name__)
diff --git a/openhcs/pyqt_gui/windows/config_window.py b/openhcs/pyqt_gui/windows/config_window.py
index 7c13b6f588..7218f0de85 100644
--- a/openhcs/pyqt_gui/windows/config_window.py
+++ b/openhcs/pyqt_gui/windows/config_window.py
@@ -7,7 +7,7 @@
from pyqt_reactive.windows.config_window import ConfigDocumentFormat, ConfigWindow
from openhcs.core.config_document import ConfigDocumentAuthority
-from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
+from formbridge.bridge.identities import UiStableWindowIdentityDeclaration
class OpenHCSConfigDocument(ConfigDocumentFormat):
@@ -35,4 +35,4 @@ class OpenHCSConfigWindow(ConfigWindow):
def window_manager_scope_id(self) -> str | None:
if self.scope_id is None:
return None
- return OpenHCSUiWindowId.agent_window_id_for_manager_scope(self.scope_id)
+ return UiStableWindowIdentityDeclaration.window_id_for_manager_scope(self.scope_id)
diff --git a/openhcs/pyqt_gui/windows/dual_editor_session.py b/openhcs/pyqt_gui/windows/dual_editor_session.py
index e4f4d83d4f..b9030bf6b4 100644
--- a/openhcs/pyqt_gui/windows/dual_editor_session.py
+++ b/openhcs/pyqt_gui/windows/dual_editor_session.py
@@ -41,13 +41,6 @@ def object_session(self) -> ObjectStateEditSession[FunctionStep]:
expected_type=FunctionStep,
)
- def current_function_spec(self) -> FunctionSpec:
- """Return the live function spec from ObjectState, falling back to the clone."""
- object_session = self.object_session()
- if object_session.has_parameter("func"):
- return cast(FunctionSpec, object_session.parameter_value("func"))
- return self.editing_step.func
-
def current_source_bindings(self):
"""Return live source bindings from ObjectState, falling back to the clone."""
object_session = self.object_session()
diff --git a/openhcs/pyqt_gui/windows/dual_editor_window.py b/openhcs/pyqt_gui/windows/dual_editor_window.py
index 6ae7a65574..ace2b60c85 100644
--- a/openhcs/pyqt_gui/windows/dual_editor_window.py
+++ b/openhcs/pyqt_gui/windows/dual_editor_window.py
@@ -21,11 +21,11 @@
QVBoxLayout,
QWidget,
)
-from pyqt_reactive.forms.layout_constants import CURRENT_LAYOUT
-from pyqt_reactive.services.function_navigation import is_function_field_path
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.forms.layout_constants import CURRENT_LAYOUT
+from formbridge.services.function_navigation import is_function_field_path
+from formbridge.services.scope_token_service import ScopeTokenService
from pyqt_reactive.services.time_travel_navigation import FunctionScopeRef, NavigationTarget
-from pyqt_reactive.services.window_code_document import WindowCodeDocumentDriver
+from formbridge.services.window_code_document import WindowCodeDocumentDriver
from pyqt_reactive.services.window_manager import WindowManager
from pyqt_reactive.services.window_navigation import (
FieldWindowNavigationDriver,
@@ -60,7 +60,7 @@
from openhcs.core.source_binding_context import SourceBindingContext
from openhcs.core.config import SourceBindingsConfig
from openhcs.core.steps.abstract import AbstractStep
-from openhcs.core.steps.function_step import FunctionSpec, FunctionStep
+from openhcs.core.steps.function_step import FunctionStep
from python_introspect import SignatureAnalyzer
from openhcs.authoring.session.step_scopes import (
StepEditorScope,
@@ -922,10 +922,6 @@ def _sync_function_editor_from_step(self):
logger.debug("✅ Triggered function editor refresh from context")
- def _current_function_spec(self) -> FunctionSpec:
- """Compatibility wrapper for tests; authority lives in DualEditorSession."""
- return self._session.current_function_spec()
-
def _invalidate_artifact_plan(self) -> None:
if self.artifact_plan_view is None:
return
diff --git a/openhcs/pyqt_gui/windows/help_window.py b/openhcs/pyqt_gui/windows/help_window.py
index 3e9de46cba..fa57e0e72c 100644
--- a/openhcs/pyqt_gui/windows/help_window.py
+++ b/openhcs/pyqt_gui/windows/help_window.py
@@ -21,7 +21,7 @@
QVBoxLayout,
QWidget,
)
-from pyqt_reactive.services.help_document import HelpDocument, HelpDocumentFormat
+from formbridge.services.help_document import HelpDocument, HelpDocumentFormat
from pyqt_reactive.theming import ColorScheme
from pyqt_reactive.widgets.help_document_browser import HelpDocumentBrowser
diff --git a/openhcs/pyqt_gui/windows/managed_windows.py b/openhcs/pyqt_gui/windows/managed_windows.py
index c66530c6b6..0822af5f0e 100644
--- a/openhcs/pyqt_gui/windows/managed_windows.py
+++ b/openhcs/pyqt_gui/windows/managed_windows.py
@@ -12,7 +12,7 @@
ImageBrowserWindowIdentity,
LogViewerWindowIdentity,
PipelineEditorWidgetIdentity,
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
ZmqServerManagerWindowIdentity,
)
@@ -32,7 +32,7 @@ def __init__(self, main_window, service_adapter):
super().__init__(main_window)
self.main_window = main_window
self.service_adapter = service_adapter
- self.setWindowTitle(PlateManagerWidgetIdentity.require_title())
+ self.setWindowTitle(DatasetListWidgetIdentity.require_title())
self.setModal(False)
self.resize(600, 400)
@@ -103,7 +103,7 @@ def _setup_connections(self):
plate_widgets = [self.main_window.embedded_widgets.require_plate_manager()]
plate_window = WindowManager._scoped_windows.get(
- PlateManagerWidgetIdentity.require_value()
+ DatasetListWidgetIdentity.require_value()
)
if plate_window is not None and plate_window.widget not in plate_widgets:
plate_widgets.append(plate_window.widget)
diff --git a/openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py b/openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py
index 70a7ca0f5d..4adcb59416 100644
--- a/openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py
+++ b/openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py
@@ -23,7 +23,7 @@
from PyQt6.QtGui import QFont
from pyqt_reactive.forms.parameter_form_manager import ParameterFormManager
-from pyqt_reactive.forms.layout_constants import CURRENT_LAYOUT
+from formbridge.forms.layout_constants import CURRENT_LAYOUT
from pyqt_reactive.theming import ColorScheme
from pyqt_reactive.widgets.shared.reflowing_vertical_scroll_area import (
ReflowingVerticalScrollArea,
diff --git a/openhcs/runtime/function_catalog_preparation.py b/openhcs/runtime/function_catalog_preparation.py
index 37712b7048..b55ccf94c5 100644
--- a/openhcs/runtime/function_catalog_preparation.py
+++ b/openhcs/runtime/function_catalog_preparation.py
@@ -193,7 +193,8 @@ def observe(
self, handle: FunctionCatalogPreparationHandle
) -> FunctionCatalogPreparationState:
"""Project this future, never wait, restart or build another job store."""
- from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError
+ from openhcs.agent.dto.common import SCHEMA_VERSION
+ from formbridge.contracts import AgentError
from openhcs.agent.dto.functions import (
FunctionCatalogPreparationOutcome as Outcome,
)
diff --git a/openhcs/runtime/napari_viewer_server.py b/openhcs/runtime/napari_viewer_server.py
index f6a3059aa6..ef4dcdf378 100644
--- a/openhcs/runtime/napari_viewer_server.py
+++ b/openhcs/runtime/napari_viewer_server.py
@@ -6104,7 +6104,7 @@ def validate_request(self, request: NapariAcceptedControlRequest) -> None:
@staticmethod
def capture_spec(message):
from python_introspect import validate_annotated_dataclass
- from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureSpec
+ from formbridge.services.window_snapshot import WindowSnapshotCaptureSpec
capture_spec = message.get(ViewerControlResponseField.PAYLOAD.value)
if not isinstance(capture_spec, WindowSnapshotCaptureSpec):
diff --git a/openhcs/runtime/viewer_snapshot.py b/openhcs/runtime/viewer_snapshot.py
index b5a4c48fd4..1ae03bc991 100644
--- a/openhcs/runtime/viewer_snapshot.py
+++ b/openhcs/runtime/viewer_snapshot.py
@@ -4,15 +4,14 @@
from dataclasses import dataclass, fields
-from pyqt_reactive.services.window_snapshot import (
- QtWindowSnapshot,
- QtWindowSnapshotService,
+from pyqt_reactive.services.window_snapshot import QtWindowSnapshot, QtWindowSnapshotService
+from formbridge.services.window_snapshot import (
WindowSnapshotObservationFailure,
WindowVisualObservation,
)
from zmqruntime.messages import ControlErrorResponse
-from openhcs.agent.dto.common import AgentResourceRef
+from formbridge.contracts import AgentResourceRef
from openhcs.agent.dto.viewer import ViewerWindowDescriptor
from python_introspect import to_jsonable
diff --git a/openhcs/serialization/pycodify_formatters.py b/openhcs/serialization/pycodify_formatters.py
index 7eae46b939..5231509a78 100644
--- a/openhcs/serialization/pycodify_formatters.py
+++ b/openhcs/serialization/pycodify_formatters.py
@@ -13,7 +13,7 @@
imported_name,
to_source,
)
-from pyqt_reactive.pattern_metadata import PatternScopeToken
+from formbridge.pattern_metadata import PatternScopeToken
from python_introspect import callable_declaration_kwargs, parameter_exclusions
from openhcs.core.axes import is_grouping_declaration
diff --git a/openhcs/utils/environment.py b/openhcs/utils/environment.py
index de0b92e4ca..85278b9eb8 100644
--- a/openhcs/utils/environment.py
+++ b/openhcs/utils/environment.py
@@ -13,7 +13,7 @@
from openhcs.resources.brand import BRAND_PRODUCT_NAME
if TYPE_CHECKING:
- from openhcs.agent.runtime_platform import AgentRuntimePlatform
+ from formbridge.runtime_platform import AgentRuntimePlatform
class OpenHCSProcessEnvironment:
@@ -44,7 +44,7 @@ def numba_cache_path(
def current_numba_cache_path(cls) -> Path:
"""Return the compiled-code cache for the current host platform."""
- from openhcs.agent.runtime_platform import AgentRuntimePlatform
+ from formbridge.runtime_platform import AgentRuntimePlatform
return cls.numba_cache_path(AgentRuntimePlatform.current())
diff --git a/openhcs/utils/performance_monitor.py b/openhcs/utils/performance_monitor.py
deleted file mode 100644
index 3156362eee..0000000000
--- a/openhcs/utils/performance_monitor.py
+++ /dev/null
@@ -1,228 +0,0 @@
-"""Performance monitoring utilities for OpenHCS.
-
-Provides decorators and context managers for timing operations and
-logging performance metrics.
-"""
-
-import time
-import functools
-import logging
-import os
-from contextlib import contextmanager
-from typing import Optional, Callable
-from pathlib import Path
-
-# Create performance logger
-perf_logger = logging.getLogger("openhcs.performance")
-perf_logger.setLevel(logging.WARNING)
-
-# Add file handler for performance logs
-perf_log_file = (
- Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
- / "openhcs"
- / "logs"
- / "performance.log"
-)
-perf_log_file.parent.mkdir(parents=True, exist_ok=True)
-
-file_handler = logging.FileHandler(perf_log_file)
-file_handler.setLevel(logging.DEBUG)
-file_handler.setFormatter(
- logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
-)
-perf_logger.addHandler(file_handler)
-
-# Also log to console
-console_handler = logging.StreamHandler()
-console_handler.setLevel(logging.WARNING)
-console_handler.setFormatter(logging.Formatter("⏱️ %(message)s"))
-perf_logger.addHandler(console_handler)
-
-
-@contextmanager
-def timer(
- operation_name: str, threshold_ms: float = 0.0, log_args: bool = False, **kwargs
-):
- """Context manager for timing operations.
-
- Args:
- operation_name: Name of the operation being timed
- threshold_ms: Only log if operation takes longer than this (in milliseconds)
- log_args: Whether to log kwargs in the message
- **kwargs: Additional context to include in log message
-
- Example:
- with timer("Loading config", threshold_ms=10.0, config_type="GlobalPipelineConfig"):
- config = load_config()
- """
- start = time.perf_counter()
- try:
- yield
- finally:
- elapsed_ms = (time.perf_counter() - start) * 1000
-
- if elapsed_ms >= threshold_ms:
- msg = f"{operation_name}: {elapsed_ms:.2f}ms"
- if log_args and kwargs:
- args_str = ", ".join(f"{k}={v}" for k, v in kwargs.items())
- msg += f" ({args_str})"
-
- perf_logger.debug(msg)
-
-
-def timed(operation_name: Optional[str] = None, threshold_ms: float = 0.0):
- """Decorator for timing function calls.
-
- Args:
- operation_name: Name for the operation (defaults to function name)
- threshold_ms: Only log if operation takes longer than this (in milliseconds)
-
- Example:
- @timed("Config loading", threshold_ms=10.0)
- def load_config():
- ...
- """
-
- def decorator(func: Callable) -> Callable:
- nonlocal operation_name
- if operation_name is None:
- operation_name = f"{func.__module__}.{func.__qualname__}"
-
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- start = time.perf_counter()
- try:
- return func(*args, **kwargs)
- finally:
- elapsed_ms = (time.perf_counter() - start) * 1000
-
- if elapsed_ms >= threshold_ms:
- perf_logger.debug(f"{operation_name}: {elapsed_ms:.2f}ms")
-
- return wrapper
-
- return decorator
-
-
-class PerformanceMonitor:
- """Accumulates timing statistics for repeated operations.
-
- Example:
- monitor = PerformanceMonitor("Placeholder resolution")
-
- for field in fields:
- with monitor.measure():
- resolve_placeholder(field)
-
- monitor.report() # Logs summary statistics
- """
-
- def __init__(self, operation_name: str):
- self.operation_name = operation_name
- self.timings = []
- self.current_start = None
-
- @contextmanager
- def measure(self):
- """Measure a single operation."""
- start = time.perf_counter()
- try:
- yield
- finally:
- elapsed_ms = (time.perf_counter() - start) * 1000
- self.timings.append(elapsed_ms)
-
- def report(self, log_individual: bool = False):
- """Log summary statistics.
-
- Args:
- log_individual: Whether to log each individual timing
- """
- if not self.timings:
- perf_logger.debug(f"{self.operation_name}: No measurements")
- return
-
- count = len(self.timings)
- total_ms = sum(self.timings)
- avg_ms = total_ms / count
- min_ms = min(self.timings)
- max_ms = max(self.timings)
-
- perf_logger.debug(
- f"{self.operation_name} - "
- f"Count: {count}, "
- f"Total: {total_ms:.2f}ms, "
- f"Avg: {avg_ms:.2f}ms, "
- f"Min: {min_ms:.2f}ms, "
- f"Max: {max_ms:.2f}ms"
- )
-
- if log_individual:
- for i, timing in enumerate(self.timings, 1):
- perf_logger.debug(f" #{i}: {timing:.2f}ms")
-
- def reset(self):
- """Clear all timings."""
- self.timings.clear()
-
-
-# Global monitors for common operations
-_monitors = {}
-
-
-def get_monitor(operation_name: str) -> PerformanceMonitor:
- """Get or create a global monitor for an operation.
-
- Example:
- monitor = get_monitor("Placeholder resolution")
- with monitor.measure():
- resolve_placeholder(field)
- """
- if operation_name not in _monitors:
- _monitors[operation_name] = PerformanceMonitor(operation_name)
- return _monitors[operation_name]
-
-
-def report_all_monitors():
- """Report statistics for all global monitors."""
- if not _monitors:
- perf_logger.debug("No performance monitors active")
- return
-
- perf_logger.debug("=" * 60)
- perf_logger.debug("PERFORMANCE SUMMARY")
- perf_logger.debug("=" * 60)
-
- for monitor in _monitors.values():
- monitor.report()
-
- perf_logger.debug("=" * 60)
-
-
-def reset_all_monitors():
- """Reset all global monitors."""
- for monitor in _monitors.values():
- monitor.reset()
-
-
-# Convenience function to enable/disable performance logging
-_enabled = True
-
-
-def enable_performance_logging():
- """Enable performance logging."""
- global _enabled
- _enabled = True
- perf_logger.setLevel(logging.DEBUG)
-
-
-def disable_performance_logging():
- """Disable performance logging."""
- global _enabled
- _enabled = False
- perf_logger.setLevel(logging.WARNING)
-
-
-def is_performance_logging_enabled() -> bool:
- """Check if performance logging is enabled."""
- return _enabled
diff --git a/pyproject.toml b/pyproject.toml
index d6f3007b95..27e5d5f075 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -99,14 +99,15 @@ dependencies = [
"psutil>=5.9.0",
# External dependencies (PyPI versions for production builds)
- "zmqruntime>=0.6.1,<0.7",
+ "zmqruntime>=0.6.2,<0.7",
"pycodify>=0.2.0,<0.3",
- "objectstate>=2.1,<3",
+ "objectstate>=2.1.1,<3",
"python-introspect>=0.2.8,<0.3",
"metaclass-registry>=0.3.0,<0.4",
"arraybridge>=0.4.1,<0.5",
- "polystore>=0.6,<0.7",
- "pyqt-reactive>=0.5,<0.6",
+ "polystore>=0.6.1,<0.7",
+ "formbridge>=0.1,<0.2",
+ "pyqt-reactive>=0.6,<0.7",
]
[project.optional-dependencies]
diff --git a/scripts/capture_media_gallery.py b/scripts/capture_media_gallery.py
index 444196de2b..cca79e48b9 100755
--- a/scripts/capture_media_gallery.py
+++ b/scripts/capture_media_gallery.py
@@ -32,13 +32,13 @@
from pyqt_reactive.animation import WindowFlashOverlay
from pyqt_reactive.animation.flash_overlay_opengl import WindowFlashOverlayGL
-from pyqt_reactive.services.tab_identity import TabLabelDeclarationMixin
+from formbridge.services.tab_identity import TabLabelDeclarationMixin
from pyqt_reactive.services.widget_tree_projection import WidgetActionKind
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from python_introspect import dataclass_from_mapping, to_jsonable
from openhcs.agent.dto.execution import ExecutionConnectionSpec
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiActionCatalog,
UiActionInvokeRequest,
UiActionInvokeResult,
@@ -61,7 +61,7 @@
ViewerWindowSnapshotRequest,
ViewerWindowSnapshotResult,
)
-from openhcs.agent.services.ui_bridge_service import (
+from formbridge.bridge.service import (
UiBridgeCloseWindowOperation,
UiBridgeInvokeActionOperation,
UiBridgeInvokeWidgetActionOperation,
diff --git a/scripts/gallery_catalog.py b/scripts/gallery_catalog.py
index c4ffb8e4ac..22e658e824 100644
--- a/scripts/gallery_catalog.py
+++ b/scripts/gallery_catalog.py
@@ -21,7 +21,7 @@
from pyqt_reactive.services.function_list_editor_actions import (
FunctionListEditorAction,
)
-from pyqt_reactive.services.function_navigation import FUNCTION_FIELD_ROOT
+from formbridge.services.function_navigation import FUNCTION_FIELD_ROOT
from pyqt_reactive.services.system_monitor_actions import SystemMonitorAction
from python_introspect import JsonValue, dataclass_from_mapping, to_jsonable
@@ -37,9 +37,9 @@
LogViewerWindowIdentity,
MainWindowWidgetIdentity,
PipelineEditorWidgetIdentity,
- PlateManagerWidgetIdentity,
- UiStableWindowIdentityDeclaration,
+ DatasetListWidgetIdentity,
)
+from formbridge.bridge.identities import UiStableWindowIdentityDeclaration
from openhcs.core.streaming_config_declarations import ViewerType
from openhcs.pyqt_gui.ui_tab_identities import DualEditorTab, PlateViewerTab
@@ -425,8 +425,8 @@ class PlateManagerActionWindowCaptureTarget(UiContextCaptureTargetABC):
action: type[SessionOperation]
tab: PlateViewerTab | None = None
- capture_widget_identity: ClassVar[type[PlateManagerWidgetIdentity]] = (
- PlateManagerWidgetIdentity
+ capture_widget_identity: ClassVar[type[DatasetListWidgetIdentity]] = (
+ DatasetListWidgetIdentity
)
def capture_source(
diff --git a/scripts/master_multi_plate_demo.py b/scripts/master_multi_plate_demo.py
index f94fbf6a27..9460d75559 100644
--- a/scripts/master_multi_plate_demo.py
+++ b/scripts/master_multi_plate_demo.py
@@ -32,9 +32,9 @@
from openhcs.agent.dto.ui_bridge import UiSelectedPlateWorkflowKind
from openhcs.agent.ui_bridge_identities import (
- PlateManagerOrchestratorCodeDocumentIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
- PlateManagerWidgetIdentity,
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+ DatasetListWidgetIdentity,
)
from openhcs.core.aligned_image_payload import AlignedImageSliceContext
from openhcs.core.artifacts.kinds import MeasurementsArtifactType
@@ -71,11 +71,11 @@
DEFAULT_BASE_PORT = 5900
UI_TIMEOUT_MS = 2_000
DEFAULT_PRESENTATION_DWELL_SECONDS = 1.0
-ORCHESTRATOR_DOCUMENT_ID = PlateManagerOrchestratorCodeDocumentIdentity.require_value()
+ORCHESTRATOR_DOCUMENT_ID = DatasetListCodeDocumentIdentity.require_value()
PLATE_MANAGER_STATE_SURFACE_ID = (
- PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ DatasetListStateSurfaceIdentity.require_value()
)
-PLATE_MANAGER_WIDGET_ID = PlateManagerWidgetIdentity.require_value()
+PLATE_MANAGER_WIDGET_ID = DatasetListWidgetIdentity.require_value()
class MasterDemoFailure(RuntimeError):
diff --git a/scripts/mcp_assay_showcase.py b/scripts/mcp_assay_showcase.py
index b0170d76d3..aeabcf76ee 100644
--- a/scripts/mcp_assay_showcase.py
+++ b/scripts/mcp_assay_showcase.py
@@ -27,9 +27,9 @@
from polystore.streaming.identity import StreamProducerIdentity
from openhcs.agent.ui_bridge_identities import (
- PlateManagerOrchestratorCodeDocumentIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
- PlateManagerWidgetIdentity,
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+ DatasetListWidgetIdentity,
)
from openhcs.core.aligned_image_payload import AlignedImageSliceContext
from openhcs.core.artifacts.kinds import (
@@ -43,7 +43,7 @@
ROOT = Path(__file__).resolve().parents[1]
PYTHON = os.environ.get("PYTHON_BIN", sys.executable)
DEFAULT_OUTPUT_ROOT = ROOT / "mcp_outputs" / "assay_showcase"
-ORCHESTRATOR_DOCUMENT_ID = PlateManagerOrchestratorCodeDocumentIdentity.require_value()
+ORCHESTRATOR_DOCUMENT_ID = DatasetListCodeDocumentIdentity.require_value()
WELL = "A01"
SHOWCASE_NAPARI_PORT = 5889
UI_TIMEOUT_MS = 2_000
@@ -1150,7 +1150,7 @@ def discover_live_measurement_surface_id(
widget_id = action.get("widget_id")
action_id = action.get("action_id")
if (
- widget_id == PlateManagerWidgetIdentity.require_value()
+ widget_id == DatasetListWidgetIdentity.require_value()
and action_id == ShowLiveResults.operation_id
):
raw_related = action.get("related_state_surface_ids")
@@ -1160,7 +1160,7 @@ def discover_live_measurement_surface_id(
for surface_id in raw_related
if isinstance(surface_id, str)
and surface_id
- != PlateManagerStateSurfaceIdentityDeclaration.require_value()
+ != DatasetListStateSurfaceIdentity.require_value()
)
surfaces = surface_catalog.get("surfaces")
@@ -1502,7 +1502,7 @@ def open_human_results_table(
"open_human_results_table",
[
"invoke-action",
- PlateManagerWidgetIdentity.require_value(),
+ DatasetListWidgetIdentity.require_value(),
ShowLiveResults.operation_id,
"--json",
*_ui_args(ctx),
@@ -1651,7 +1651,7 @@ def read_live_measurements() -> tuple[str, dict[str, Any], dict[str, Any]]:
"list_plate_manager_actions",
[
"actions",
- PlateManagerWidgetIdentity.require_value(),
+ DatasetListWidgetIdentity.require_value(),
"--json",
*_ui_args(ctx),
],
diff --git a/scripts/mcp_thesis_demo_live.py b/scripts/mcp_thesis_demo_live.py
index bb9dd643e5..fdb6d2ae81 100755
--- a/scripts/mcp_thesis_demo_live.py
+++ b/scripts/mcp_thesis_demo_live.py
@@ -29,7 +29,7 @@
# isort: split
from pyqt_reactive.dialogs.group_by_selector_dialog import GroupBySelectorDialog
-from pyqt_reactive.services.function_navigation import FunctionPatternField
+from formbridge.services.function_navigation import FunctionPatternField
from pyqt_reactive.widgets.shared import DetachableActionBar, ManagedWindowAction
from zmqruntime import (
EndpointShutdownMode,
@@ -60,19 +60,19 @@
UiSnapshotWindowCapability,
ValidateViewerWindowStateCapability,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiActionIdentity,
UiActionSummary,
UiCodeDocumentSummary,
UiWindowSummary,
)
from python_introspect import dataclass_from_mapping
+from formbridge.bridge.identities import ManagedWindowWidgetIdentity
from openhcs.agent.ui_bridge_identities import (
- ManagedWindowWidgetIdentity,
PipelineEditorStateSurfaceIdentityDeclaration,
- PlateManagerOrchestratorCodeDocumentIdentity,
- PlateManagerStateSurfaceIdentityDeclaration,
- PlateManagerWidgetIdentity,
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+ DatasetListWidgetIdentity,
)
from openhcs.core.config_cache import ConfigCacheSpec, save_config_sync
from openhcs.ui.shared.plate_manager_code_document import PlateManagerDocumentAssignments
@@ -99,6 +99,7 @@
)
from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG, OpenHCSZMQConfig
from openhcs.domains.microscopy.axes import Microscopy
+from formbridge.bridge.identities import UiStableWindowIdentityDeclaration
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DEMO_ROOT = ROOT / "mcp_outputs" / "thesis_demo" / "live"
@@ -111,7 +112,7 @@
/ "test_main[ImageXpress]"
/ "zstack_plate"
)
-ORCHESTRATOR_DOCUMENT_ID = PlateManagerOrchestratorCodeDocumentIdentity.require_value()
+ORCHESTRATOR_DOCUMENT_ID = DatasetListCodeDocumentIdentity.require_value()
PYTHON = os.environ.get("PYTHON_BIN", sys.executable)
FINAL_DEMO_STEP_DISPLAY_INDEX = 8
FINAL_DEMO_STEP_ROUTE_INDEX = FINAL_DEMO_STEP_DISPLAY_INDEX - 1
@@ -984,7 +985,7 @@ def managed_window_action_target(
)
target_scope_ids = matches[0].target_scope_ids
window_scopes = frozenset(
- OpenHCSUiWindowId.manager_scopes_for_agent_window_id(window_id)
+ UiStableWindowIdentityDeclaration.manager_scopes_for_window_id(window_id)
)
advertised_targets = [
scope_id
@@ -1850,7 +1851,7 @@ def inspect_and_apply_code_document(
for doc in documents
):
raise RehearsalFailure(
- "Writable plate_manager.orchestrator_config document missing."
+ "Writable datasets.orchestrator_config document missing."
)
source_payload = command_json(
@@ -2542,7 +2543,7 @@ def selected_plate_state(ctx: RunContext) -> dict[str, Any]:
"selected_plate_state",
mcp_cmd(
UiGetStateSurfaceCapability.cli_command,
- PlateManagerStateSurfaceIdentityDeclaration.require_value(),
+ DatasetListStateSurfaceIdentity.require_value(),
"--selection-mode",
"selected",
"--timeout-seconds",
@@ -2760,7 +2761,7 @@ def plate_action_summary(
f"list_{action.operation_id}_action",
mcp_cmd(
UiListActionsCapability.cli_command,
- PlateManagerWidgetIdentity.require_value(),
+ DatasetListWidgetIdentity.require_value(),
descriptor=ctx.descriptor_path,
),
timeout=30,
@@ -2774,7 +2775,7 @@ def plate_action_summary(
matches = [
row
for row in summaries
- if row.identity.widget_id == PlateManagerWidgetIdentity.require_value()
+ if row.identity.widget_id == DatasetListWidgetIdentity.require_value()
and row.identity.action_id == action.operation_id
]
if len(matches) != 1:
@@ -2794,7 +2795,7 @@ def invoke_plate_action(ctx: RunContext, action: type[SessionOperation]) -> dict
mcp_call_tool_cmd(
UiInvokeActionCapability.name,
{
- "widget_id": PlateManagerWidgetIdentity.require_value(),
+ "widget_id": DatasetListWidgetIdentity.require_value(),
"action_id": action.operation_id,
"target_scope_ids": list(summary.target_scope_ids),
"observed_selection_revision_token": summary.selection_revision_token,
@@ -4913,7 +4914,7 @@ def build_objective_evidence(report: Mapping[str, Any]) -> dict[str, Any]:
"requirement": "ui_visible_hierarchical_configuration",
"public_evidence": [
ORCHESTRATOR_DOCUMENT_ID,
- PlateManagerStateSurfaceIdentityDeclaration.require_value(),
+ DatasetListStateSurfaceIdentity.require_value(),
],
"satisfied": True,
},
diff --git a/scripts/smoke_installed_gui.py b/scripts/smoke_installed_gui.py
index aa543cd014..faf0dce9af 100644
--- a/scripts/smoke_installed_gui.py
+++ b/scripts/smoke_installed_gui.py
@@ -25,7 +25,7 @@
from zmqruntime import OperationDeadline
from openhcs.agent.dto.execution import RuntimeServerInfo
- from openhcs.agent.dto.ui_bridge import (
+ from formbridge.bridge.dto import (
UiActionInvokeResult,
UiWindowCatalog,
UiWindowSnapshotResult,
@@ -461,7 +461,7 @@ def _typed_tool_payload(
def require_ui_action_accepted(action_result: UiActionInvokeResult) -> None:
"""Fail the smoke unless the live GUI took the requested action."""
- from openhcs.agent.dto.ui_bridge import ActionAccepted
+ from formbridge.bridge.dto import ActionAccepted
if action_result.status is not ActionAccepted:
raise AssertionError(f"Installed UI action was not accepted: {action_result}")
@@ -481,10 +481,10 @@ def _capture_installed_gui_snapshot(
) -> UiWindowSnapshotResult:
"""Capture one live window through the production MCP capability."""
- from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+ from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from openhcs.agent.capabilities import UiSnapshotWindowCapability
- from openhcs.agent.dto.ui_bridge import UiWindowCatalog, UiWindowSnapshotResult
+ from formbridge.bridge.dto import UiWindowCatalog, UiWindowSnapshotResult
from openhcs.mcp.dev_client_commands.ui import WindowSnapshotCommandSpec
execution = client.execute(
@@ -566,7 +566,7 @@ def run_installed_live_mcp_smoke(
agent_capabilities,
)
from openhcs.agent.dto.mcp import McpServerHealthResult
- from openhcs.agent.dto.ui_bridge import (
+ from formbridge.bridge.dto import (
UiActionInvokeResult,
UiBridgeCatalog,
UiBridgeStatus,
@@ -575,7 +575,7 @@ def run_installed_live_mcp_smoke(
from openhcs.agent.services.runtime_server_service import RuntimeServerService
from openhcs.agent.ui_bridge_identities import (
MainWindowWidgetIdentity,
- PlateManagerWidgetIdentity,
+ DatasetListWidgetIdentity,
)
from openhcs.mcp.dev_client import McpDevClient
from openhcs.mcp.dev_client_commanding import CapabilityBackedCommandSpec
@@ -675,7 +675,7 @@ def run_installed_live_mcp_smoke(
required_window_ids = {
MainWindowWidgetIdentity.require_value(),
- PlateManagerWidgetIdentity.require_value(),
+ DatasetListWidgetIdentity.require_value(),
}
observed_window_ids = _window_ids(windows_before)
if not required_window_ids <= observed_window_ids:
@@ -689,7 +689,7 @@ def run_installed_live_mcp_smoke(
action_execution = client.execute(
(
InvokeActionCommandSpec.command,
- PlateManagerWidgetIdentity.require_value(),
+ DatasetListWidgetIdentity.require_value(),
ShowDatasetCode.operation_id,
"--request-token",
"installed-gui-mcp-smoke",
@@ -803,7 +803,7 @@ def run_installed_gui_smoke(
from zmqruntime import OperationDeadline
import openhcs
- from openhcs.agent.ui_bridge_environment import UiBridgeDescriptorEnvironment
+ from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
from openhcs.core.config import GlobalPipelineConfig
from openhcs.pyqt_gui.app import OpenHCSPyQtApp
from openhcs.pyqt_gui.config import (
@@ -828,7 +828,7 @@ def run_installed_gui_smoke(
),
descriptor_path=descriptor_path,
)
- os.environ[UiBridgeDescriptorEnvironment.descriptor_file_path_key] = str(
+ os.environ[OpenHCSUiBridgeApplication.descriptor_file_variable] = str(
descriptor_path
)
runtime_context = PyQtGuiRuntimeContext(
diff --git a/scripts/validate_docs.py b/scripts/validate_docs.py
index 8205bb185d..368955fe5a 100644
--- a/scripts/validate_docs.py
+++ b/scripts/validate_docs.py
@@ -49,6 +49,7 @@
/ "metaclass_registry"
),
"polystore": REPOSITORY_ROOT / "external" / "PolyStore" / "src" / "polystore",
+ "formbridge": REPOSITORY_ROOT / "external" / "formbridge" / "src" / "formbridge",
"pyqt_reactive": (
REPOSITORY_ROOT / "external" / "pyqt-reactive" / "src" / "pyqt_reactive"
),
diff --git a/tests/diagnostics/check_viewer_feature_measurement_live.py b/tests/diagnostics/check_viewer_feature_measurement_live.py
index 15dac4e33e..eb5c44b9ba 100644
--- a/tests/diagnostics/check_viewer_feature_measurement_live.py
+++ b/tests/diagnostics/check_viewer_feature_measurement_live.py
@@ -201,7 +201,7 @@ def save() -> None:
from openhcs.core.streaming_config_declarations import NapariViewer
from zmqruntime.config import TransportMode
from python_introspect import to_jsonable
- from openhcs.agent.runtime_platform import AgentRuntimePlatform
+ from formbridge.runtime_platform import AgentRuntimePlatform
from polystore.imagej_distribution import (
FijiArchiveDistribution,
ImageJArchiveDownloadPolicy,
diff --git a/tests/fixtures/mcp/mcp_tool_contract.json b/tests/fixtures/mcp/mcp_tool_contract.json
index f1a6682793..8de654d693 100644
--- a/tests/fixtures/mcp/mcp_tool_contract.json
+++ b/tests/fixtures/mcp/mcp_tool_contract.json
@@ -8822,11 +8822,13 @@
"type": "string"
},
"surface_id": {
- "default": "plate_manager.state",
"title": "Surface Id",
"type": "string"
}
},
+ "required": [
+ "surface_id"
+ ],
"title": "openhcs_ui_get_state_surfaceArguments",
"type": "object"
},
diff --git a/tests/pyqt_gui/test_main_window_offscreen_smoke.py b/tests/pyqt_gui/test_main_window_offscreen_smoke.py
index ececabcde9..af7207d343 100644
--- a/tests/pyqt_gui/test_main_window_offscreen_smoke.py
+++ b/tests/pyqt_gui/test_main_window_offscreen_smoke.py
@@ -188,11 +188,6 @@ def startup_failed(error: BaseException) -> None:
if __name__ == "__main__":
exit_code, app = _run_main_window(int(sys.argv[1]))
- # Tearing down the QApplication (PyQt's exit handler and interpreter
- # finalization) intermittently segfaults with no Python frame, before and
- # after U5 alike; recorded in U5-dead-ui-code.md. No exit handler owns a
- # process, so leave without teardown: this test checks the application's own
- # exit status and the processes it leaves behind.
- sys.stdout.flush()
- sys.stderr.flush()
- os._exit(exit_code)
+ # Normal interpreter teardown: a crash while the QApplication is torn down
+ # fails this test through the child's exit status.
+ sys.exit(exit_code)
diff --git a/tests/unit/agent/test_agent_services.py b/tests/unit/agent/test_agent_services.py
index 3a1fd07fe4..032bf1c3ef 100644
--- a/tests/unit/agent/test_agent_services.py
+++ b/tests/unit/agent/test_agent_services.py
@@ -8,10 +8,8 @@
import pytest
from polystore.virtual_workspace import SourcePixelRef
-from pyqt_reactive.services.parameter_help_service import (
- dataclass_parameter_descriptions,
-)
-from pyqt_reactive.services.window_snapshot import (
+from formbridge.services.parameter_help_service import dataclass_parameter_descriptions
+from formbridge.services.window_snapshot import (
WindowSnapshotCaptureScope,
WindowSnapshotFrameCondition,
)
diff --git a/tests/unit/agent/test_config_reference_service.py b/tests/unit/agent/test_config_reference_service.py
index 1df8df155f..4bffeffefd 100644
--- a/tests/unit/agent/test_config_reference_service.py
+++ b/tests/unit/agent/test_config_reference_service.py
@@ -1,7 +1,7 @@
from dataclasses import fields
from objectstate import ObjectState
-from pyqt_reactive.services.parameter_help_service import dataclass_parameter_descriptions
+from formbridge.services.parameter_help_service import dataclass_parameter_descriptions
from openhcs.agent.dto.knowledge import KnowledgeBaseDocumentRequest
from openhcs.agent.services.config_reference_service import (
diff --git a/tests/unit/agent/test_function_declaration_help.py b/tests/unit/agent/test_function_declaration_help.py
index 66b73b7698..8adce61fc8 100644
--- a/tests/unit/agent/test_function_declaration_help.py
+++ b/tests/unit/agent/test_function_declaration_help.py
@@ -7,8 +7,8 @@
import pytest
-from pyqt_reactive.services.help_document import HelpDocument
-from pyqt_reactive.services.parameter_help_service import docstring_info_for_target
+from formbridge.services.help_document import HelpDocument
+from formbridge.services.parameter_help_service import docstring_info_for_target
from openhcs.agent.services.function_catalog_service import (
FunctionCatalogService,
@@ -175,9 +175,7 @@ class AuditedPolicy(AnnotationAuditPolicy, SupplierAuditPolicy):
def test_policy_consumer_executes_cooperative_hooks_once_in_declared_mro():
- from pyqt_reactive.services.parameter_help_service import (
- dataclass_type_from_annotation,
- )
+ from formbridge.services.parameter_help_service import dataclass_type_from_annotation
from openhcs.agent.dto.functions import (
SuppliedByAgent,
SuppliedFromPrimaryInput,
diff --git a/tests/unit/agent/test_mcp_affine_inspection_progress.py b/tests/unit/agent/test_mcp_affine_inspection_progress.py
index a1e9a9bf61..fd8303c693 100644
--- a/tests/unit/agent/test_mcp_affine_inspection_progress.py
+++ b/tests/unit/agent/test_mcp_affine_inspection_progress.py
@@ -6,7 +6,6 @@
import time
from types import SimpleNamespace
-from PyQt6.QtCore import QCoreApplication, QThread
import pytest
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
@@ -16,13 +15,14 @@
InspectPipelineSourceArtifactPlanCapability,
MainThreadProgressCapability,
)
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.execution import ArtifactPlanInspection
from openhcs.mcp.execution import (
McpMainThreadDispatcher, McpQueuedCallNotStartedError, McpTransportExecutor,
)
-from pyqt_reactive.services.ui_thread_dispatch import (
- UiThreadDispatchError, UiThreadDispatcherClosedError, UiThreadDispatchTimeoutError,
+from formbridge.services.main_thread_dispatch import (
+ MainThreadDispatcherClosedError, MainThreadDispatchTimeoutError,
)
from openhcs.mcp.server import build_server
from python_introspect import to_jsonable
@@ -52,7 +52,6 @@ def __init__(self, request_identity, *, error=None):
def inspect(self, request):
assert threading.current_thread() is threading.main_thread()
- assert QThread.currentThread() == QCoreApplication.instance().thread()
self.calls.append((request, self.request_identity.get()))
EndpointStartupStatus(
EndpointStartupPhase.PREPARING_CAPABILITIES,
@@ -236,7 +235,7 @@ async def exercise():
def test_callback_timeout_after_side_effect_is_not_prestart_cancellation(queued):
executor = McpTransportExecutor()
calls = []
- original = UiThreadDispatchTimeoutError("Callback timed out after its side effect.")
+ original = MainThreadDispatchTimeoutError("Callback timed out after its side effect.")
def callback():
calls.append("side-effect")
@@ -246,7 +245,7 @@ async def exercise():
return await asyncio.to_thread(executor.dispatcher.call, callback)
try:
- with pytest.raises(UiThreadDispatchTimeoutError) as caught:
+ with pytest.raises(MainThreadDispatchTimeoutError) as caught:
if queued:
executor.run(exercise)
else:
@@ -264,7 +263,7 @@ def test_closed_dispatcher_rejects_without_invocation():
executor.dispatcher.close()
async def exercise():
- with pytest.raises(UiThreadDispatcherClosedError):
+ with pytest.raises(MainThreadDispatcherClosedError):
await executor.dispatcher.invoke(lambda: calls.append("forbidden"))
try:
@@ -274,14 +273,15 @@ async def exercise():
executor.close()
-def test_headless_absence_does_not_authorize_worker_affinity(monkeypatch):
- monkeypatch.setattr(QCoreApplication, "instance", lambda: None)
+def test_worker_call_never_runs_on_the_worker_thread():
dispatcher = McpMainThreadDispatcher()
calls = []
async def exercise():
- with pytest.raises(UiThreadDispatchError, match="No Qt application"):
- await asyncio.to_thread(dispatcher.call, lambda: calls.append("forbidden"))
+ with pytest.raises(McpQueuedCallNotStartedError):
+ await asyncio.to_thread(
+ dispatcher.call, lambda: calls.append("forbidden"), timeout_ms=20
+ )
try:
asyncio.run(exercise())
@@ -362,7 +362,7 @@ def serve_stdio_inspection_fixture(*, work_seconds=2.4):
def test_resident_continuous_connections_keep_affinity_and_wire_progress(
unix_socket_dir, work_seconds, idle_seconds, connection_count,
):
- from pyqt_reactive.services.async_operation_executor import AsyncOperationExecutor
+ from formbridge.services.async_operation_executor import AsyncOperationExecutor
from openhcs.mcp.dev_client_core import McpDevServerSpec, McpDevSocketSession
from openhcs.mcp.socket import McpSocketTransport, wait_for_socket
import io
diff --git a/tests/unit/agent/test_mcp_affine_real_compile.py b/tests/unit/agent/test_mcp_affine_real_compile.py
index aed75c5e99..2456133df9 100644
--- a/tests/unit/agent/test_mcp_affine_real_compile.py
+++ b/tests/unit/agent/test_mcp_affine_real_compile.py
@@ -5,7 +5,6 @@
import threading
from types import SimpleNamespace
-from PyQt6.QtCore import QCoreApplication, QThread
from openhcs.agent.capabilities import InspectPipelineSourceArtifactPlanCapability
from openhcs.agent.path_policy import AgentPathPolicy
@@ -47,7 +46,6 @@ def forbidden_catalog(*args, **kwargs):
class AffineCompileGateway(InProcessCompileInspectionGateway):
def compile(self, request):
assert threading.current_thread() is threading.main_thread()
- assert QThread.currentThread() == QCoreApplication.instance().thread()
assert identity.get() == "exact-source-request"
calls.append(request)
return super().compile(request)
diff --git a/tests/unit/agent/test_mcp_capability_discovery_and_widget_projection.py b/tests/unit/agent/test_mcp_capability_discovery_and_widget_projection.py
index 395cfbcfd6..4147a8914b 100644
--- a/tests/unit/agent/test_mcp_capability_discovery_and_widget_projection.py
+++ b/tests/unit/agent/test_mcp_capability_discovery_and_widget_projection.py
@@ -1,7 +1,7 @@
import asyncio
from dataclasses import fields
-from pyqt_reactive.services.widget_tree_projection_config import (
+from formbridge.services.widget_tree_projection_config import (
COMPACT_FIELD_PROJECTION_METADATA_KEY,
CompactFieldProjection,
WidgetNodeIdentity,
@@ -20,7 +20,7 @@
DesktopLocalCapabilitySurfaceProfile,
get_capability_registry,
)
-from openhcs.agent.dto.ui_bridge import UiWidgetActionSummary
+from formbridge.bridge.dto import UiWidgetActionSummary
import openhcs.mcp.server as server
diff --git a/tests/unit/agent/test_mcp_client_registration.py b/tests/unit/agent/test_mcp_client_registration.py
index 1f4b824f1f..d8956aa02c 100644
--- a/tests/unit/agent/test_mcp_client_registration.py
+++ b/tests/unit/agent/test_mcp_client_registration.py
@@ -7,7 +7,7 @@
import pytest
import tomlkit
-from openhcs.agent.runtime_platform import (
+from formbridge.runtime_platform import (
AgentRuntimePlatform,
LinuxRuntimePlatform,
MacOSRuntimePlatform,
diff --git a/tests/unit/agent/test_mcp_dev_client_declared_decoder.py b/tests/unit/agent/test_mcp_dev_client_declared_decoder.py
index 27b23fca22..7247b3271e 100644
--- a/tests/unit/agent/test_mcp_dev_client_declared_decoder.py
+++ b/tests/unit/agent/test_mcp_dev_client_declared_decoder.py
@@ -7,7 +7,8 @@
import pytest
from openhcs.agent.capabilities import AgentResultFamilyContract, StartOwnedRuntimeCapability
-from openhcs.agent.dto.common import AgentError, AgentResultEnvelope, SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentResultEnvelope
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.execution import RuntimeBootstrapState, RuntimeBootstrapHandle
from openhcs.agent.dto.mcp import McpToolErrorResult
from openhcs.mcp.dev_client_core import (
diff --git a/tests/unit/agent/test_mcp_dev_client_generated_inputs.py b/tests/unit/agent/test_mcp_dev_client_generated_inputs.py
index e97691d575..864c67d969 100644
--- a/tests/unit/agent/test_mcp_dev_client_generated_inputs.py
+++ b/tests/unit/agent/test_mcp_dev_client_generated_inputs.py
@@ -12,7 +12,7 @@
AgentCapabilityDeclaration,
StartFunctionCatalogPreparationCapability,
)
-from openhcs.agent.dto.common import AgentDataclassCliRequest
+from formbridge.contracts import AgentDataclassCliRequest
from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec
from openhcs.agent.dto.functions import FunctionCatalogPreparationHandle
from openhcs.mcp.dev_client import _build_parser, _calls_from_args
@@ -194,7 +194,7 @@ class ExtendedConnection(*bases, ExecutionConnectionSpec):
request_tag: str = "new"
trace_enabled: bool = True
- import openhcs.agent.dto.common as common
+ import formbridge.contracts as common
original_decode = common.dataclass_from_mapping
diff --git a/tests/unit/agent/test_mcp_dev_client_knowledge_renderers.py b/tests/unit/agent/test_mcp_dev_client_knowledge_renderers.py
index 183712084a..9a2d628bc4 100644
--- a/tests/unit/agent/test_mcp_dev_client_knowledge_renderers.py
+++ b/tests/unit/agent/test_mcp_dev_client_knowledge_renderers.py
@@ -14,7 +14,8 @@
InternalApiSymbol,
)
from openhcs.agent.dto.authoring import AuthoringContext
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.functions import (
FunctionArtifactSpec,
FunctionCatalogEntry,
@@ -33,7 +34,7 @@
KnowledgeBaseSectionSummary,
KnowledgeBaseSourceSpan,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
DirtyFields,
UiCatalogPageMetadata,
UiMutationReceipt,
diff --git a/tests/unit/agent/test_mcp_dev_client_pipeline_results.py b/tests/unit/agent/test_mcp_dev_client_pipeline_results.py
index bb4bb7f9e2..c77a27ee63 100644
--- a/tests/unit/agent/test_mcp_dev_client_pipeline_results.py
+++ b/tests/unit/agent/test_mcp_dev_client_pipeline_results.py
@@ -13,12 +13,8 @@
from zmqruntime.execution import ExecutionProgressObservation
from openhcs.agent.capabilities import agent_capabilities
-from openhcs.agent.dto.common import (
- SCHEMA_VERSION,
- AgentError,
- AgentWarning,
- RenderedSource,
-)
+from openhcs.agent.dto.common import SCHEMA_VERSION, RenderedSource
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.execution import (
ArtifactInputPlanSummary,
ArtifactMaterializationPathSummary,
@@ -671,11 +667,13 @@ def forbidden_serialization(value):
def test_generic_call_preserves_pending_workflow_native_receipt(monkeypatch, capsys):
- from openhcs.agent.dto.ui_bridge import (
+ from formbridge.bridge.dto import (
UiActionIdentity,
UiActionInvokeResult,
UiMutationReceipt,
UiMutationRequestToken,
+ )
+ from openhcs.agent.dto.ui_bridge import (
UiSelectedPlateWorkflowKind,
UiSelectedPlateWorkflowResult,
)
@@ -685,7 +683,7 @@ def test_generic_call_preserves_pending_workflow_native_receipt(monkeypatch, cap
UiSelectedPlateWorkflowKind("run_datasets"),
UiActionInvokeResult(
SCHEMA_VERSION,
- UiActionIdentity(widget_id="plate_manager", action_id="run_datasets"),
+ UiActionIdentity(widget_id="datasets", action_id="run_datasets"),
"accepted",
UiMutationReceipt(UiMutationRequestToken(), accepted=True),
target_scope_ids=("scope-1",),
diff --git a/tests/unit/agent/test_mcp_dev_client_plate_renderers.py b/tests/unit/agent/test_mcp_dev_client_plate_renderers.py
index fe3992bbd1..b609021069 100644
--- a/tests/unit/agent/test_mcp_dev_client_plate_renderers.py
+++ b/tests/unit/agent/test_mcp_dev_client_plate_renderers.py
@@ -11,7 +11,8 @@
from zmqruntime.config import TransportMode
from openhcs.agent.capabilities import agent_capabilities
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec
from openhcs.agent.dto.plate import (
PlateFileQueryRecordSummary,
diff --git a/tests/unit/agent/test_mcp_dev_client_renderers.py b/tests/unit/agent/test_mcp_dev_client_renderers.py
index 0ec6bdbc6b..0cde3bee5b 100644
--- a/tests/unit/agent/test_mcp_dev_client_renderers.py
+++ b/tests/unit/agent/test_mcp_dev_client_renderers.py
@@ -21,7 +21,7 @@
import pytest
from python_introspect import to_jsonable
-from openhcs.agent.dto.common import AgentError, AgentWarning
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.mcp.dev_client_core import (
McpDevServerIdentity,
McpDevToolBatchResponse,
@@ -176,7 +176,7 @@ def test_every_renderer_reports_a_missing_payload_as_unavailable(renderer):
def test_base_appends_payload_warnings_and_declared_errors_once():
- from openhcs.agent.dto.ui_bridge import UiCodeDocumentValidationResult
+ from formbridge.bridge.dto import UiCodeDocumentValidationResult
payload = UiCodeDocumentValidationResult(
document_id="doc",
diff --git a/tests/unit/agent/test_mcp_dev_client_runtime_debug_renderer.py b/tests/unit/agent/test_mcp_dev_client_runtime_debug_renderer.py
index 6ee68e8b88..a87b0fc9b7 100644
--- a/tests/unit/agent/test_mcp_dev_client_runtime_debug_renderer.py
+++ b/tests/unit/agent/test_mcp_dev_client_runtime_debug_renderer.py
@@ -1,6 +1,6 @@
"""Focused compact rendering tests for paused runtime debug inspection."""
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from openhcs.agent.dto.execution import (
ExecutionConnectionSpec,
RuntimeDebugInspectionResult,
diff --git a/tests/unit/agent/test_mcp_dev_client_sampling_results.py b/tests/unit/agent/test_mcp_dev_client_sampling_results.py
index ffb7621857..f93b39df87 100644
--- a/tests/unit/agent/test_mcp_dev_client_sampling_results.py
+++ b/tests/unit/agent/test_mcp_dev_client_sampling_results.py
@@ -13,7 +13,8 @@
import pytest
from openhcs.agent.capabilities import SamplePlateImageCapability, agent_capabilities
-from openhcs.agent.dto.common import AgentError, SCHEMA_VERSION
+from formbridge.contracts import AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.plate import PlateImageSampleResult, SelectedPlateImageSampleResult
from openhcs.agent.dto.viewer import ViewerWindowImageSampleRecord, ViewerWindowImageSampleResult
from openhcs.mcp import dev_client
diff --git a/tests/unit/agent/test_mcp_dev_client_ui_action_results.py b/tests/unit/agent/test_mcp_dev_client_ui_action_results.py
index f7b4c6d5bc..276e213cef 100644
--- a/tests/unit/agent/test_mcp_dev_client_ui_action_results.py
+++ b/tests/unit/agent/test_mcp_dev_client_ui_action_results.py
@@ -8,7 +8,7 @@
from openhcs.agent.capabilities import agent_capabilities
from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiActionIdentity,
UiActionInvokeResult,
UiBridgeOperationIdentity,
@@ -16,9 +16,6 @@
UiBridgeOperationRoute,
UiMutationReceipt,
UiMutationRequestToken,
- UiPlateManagerState,
- UiSelectedPlateWorkflowKind,
- UiSelectedPlateWorkflowResult,
UiStateSurfaceDocument,
UiStateSurfaceIdentity,
UiStateSurfaceSummary,
@@ -26,6 +23,11 @@
UiWindowIdentity,
UiWindowSummary,
)
+from openhcs.agent.dto.ui_bridge import (
+ UiPlateManagerState,
+ UiSelectedPlateWorkflowKind,
+ UiSelectedPlateWorkflowResult,
+)
from openhcs.mcp import dev_client
from openhcs.mcp.dev_client_core import (
McpDevServerSpec, McpDevToolBatchResponse, McpDevToolResult,
@@ -42,7 +44,7 @@
def action_fixture():
return UiActionInvokeResult(
SCHEMA_VERSION,
- UiActionIdentity(widget_id="plate_manager", action_id="run_datasets"),
+ UiActionIdentity(widget_id="datasets", action_id="run_datasets"),
"accepted",
UiMutationReceipt(UiMutationRequestToken(), accepted=True),
target_scope_ids=("scope-1",),
@@ -57,8 +59,8 @@ def action_result(capability, value):
def state_fixture(compiled, revision):
summary = UiStateSurfaceSummary(
- SCHEMA_VERSION, UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- "Plate Manager", True, widget_id="plate_manager",
+ SCHEMA_VERSION, UiStateSurfaceIdentity(surface_id="datasets.state"),
+ "Plate Manager", True, widget_id="datasets",
)
row = DatasetRowState(
scope_id="scope-1", name="plate-one", root="/plate-one",
@@ -89,7 +91,7 @@ def test_named_cli_wait_preserves_action_receipt_poll_and_final_rows(
native = UiSelectedPlateWorkflowResult(
SCHEMA_VERSION, UiSelectedPlateWorkflowKind("compile_datasets"),
replace(action_fixture(),
- identity=UiActionIdentity(widget_id="plate_manager", action_id="compile_datasets"),
+ identity=UiActionIdentity(widget_id="datasets", action_id="compile_datasets"),
receipt=UiMutationReceipt(UiMutationRequestToken(), "operation-one", True)),
)
receipt = UiBridgeOperationRef(
@@ -200,7 +202,7 @@ def forbidden_serialization(value):
"--arguments", '{"workflow":"run_datasets"}',
]) == 0
rendered = capsys.readouterr().out
- assert "action=plate_manager/run_datasets status=accepted" in rendered
+ assert "action=datasets/run_datasets status=accepted" in rendered
assert "accepted=True" in rendered and "targets=scope-1" in rendered
assert workflow_result_action_status(result) == "accepted"
assert workflow_result_target_scope_ids(result) == ("scope-1",)
@@ -284,7 +286,7 @@ class ExtendedAction(UiActionInvokeResult):
try:
assert McpDevOutputRenderer.for_output_contract(ExtendedAction) is extension
value = ExtendedAction(
- SCHEMA_VERSION, UiActionIdentity(widget_id="plate_manager", action_id="run_datasets"),
+ SCHEMA_VERSION, UiActionIdentity(widget_id="datasets", action_id="run_datasets"),
"accepted", UiMutationReceipt(UiMutationRequestToken(), accepted=True),
)
decoded = McpDevToolResult._decode_payload(to_jsonable(value), (ExtendedAction,))
diff --git a/tests/unit/agent/test_mcp_dev_client_ui_renderers.py b/tests/unit/agent/test_mcp_dev_client_ui_renderers.py
index 5f8a8a6057..9c6eeb95df 100644
--- a/tests/unit/agent/test_mcp_dev_client_ui_renderers.py
+++ b/tests/unit/agent/test_mcp_dev_client_ui_renderers.py
@@ -7,9 +7,9 @@
from python_introspect import to_jsonable
-from openhcs.agent.dto.common import AgentError, AgentWarning
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.mcp import McpServerHealthResult
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
QT_TOP_LEVEL_WINDOW_KIND,
UiActionCatalog,
UiActionIdentity,
@@ -21,10 +21,6 @@
UiCodeDocumentCatalog,
UiCodeDocumentIdentity,
UiCodeDocumentSummary,
- UiDebugRuntimeFrameState,
- UiPipelineDebugSessionState,
- UiPipelineEditorState,
- UiPlateManagerState,
UiSnapshotRef,
UiStateSurfaceCatalog,
UiStateSurfaceDocument,
@@ -37,9 +33,15 @@
UiWindowIdentity,
UiWindowSummary,
)
+from openhcs.agent.dto.ui_bridge import (
+ UiDebugRuntimeFrameState,
+ UiPipelineDebugSessionState,
+ UiPipelineEditorState,
+ UiPlateManagerState,
+)
from openhcs.agent.ui_bridge_identities import (
PipelineEditorStateSurfaceIdentityDeclaration,
- PlateManagerStateSurfaceIdentityDeclaration,
+ DatasetListStateSurfaceIdentity,
)
from openhcs.mcp.dev_client_core import (
McpDevServerIdentity,
@@ -183,7 +185,7 @@ def test_catalogs_filter_and_truncate_with_contains_and_limit():
surfaces = replace(
sample_dto(UiStateSurfaceCatalog),
surfaces=(
- surface_summary("plate_manager.state", title="Plate Manager"),
+ surface_summary("datasets.state", title="Plate Manager"),
surface_summary("plate_manager.debug", title="Debug"),
surface_summary("pipeline_editor.state", title="Pipeline Editor"),
),
@@ -191,7 +193,7 @@ def test_catalogs_filter_and_truncate_with_contains_and_limit():
rendered = render(surfaces, CatalogRenderOptions(contains="plate", limit=1))
assert "State surfaces: count=3 matched=2 shown=1" in rendered
assert "Filter: contains=plate" in rendered
- assert "- plate_manager.state: widget=" in rendered
+ assert "- datasets.state: widget=" in rendered
assert "plate_manager.debug" not in rendered
assert "..." in rendered
@@ -203,7 +205,7 @@ def document(document_id):
documents = replace(
sample_dto(UiCodeDocumentCatalog),
- documents=(document("plate_manager.orchestrator_config"), document("pipeline")),
+ documents=(document("datasets.orchestrator_config"), document("pipeline")),
)
assert "Code documents: count=2 matched=2 shown=2" in render(documents)
@@ -221,7 +223,7 @@ def test_state_surface_documents_render_through_their_surface_state():
state = replace(
sample_dto(UiPlateManagerState),
summary=surface_summary(
- PlateManagerStateSurfaceIdentityDeclaration.require_value(),
+ DatasetListStateSurfaceIdentity.require_value(),
current_selection_count=1,
),
manager_execution_state="idle",
diff --git a/tests/unit/agent/test_mcp_dev_client_viewer_renderers.py b/tests/unit/agent/test_mcp_dev_client_viewer_renderers.py
index 7b8469ba12..bb32c72a39 100644
--- a/tests/unit/agent/test_mcp_dev_client_viewer_renderers.py
+++ b/tests/unit/agent/test_mcp_dev_client_viewer_renderers.py
@@ -6,14 +6,15 @@
from python_introspect import dataclass_from_mapping
from openhcs.agent.capabilities import agent_capabilities
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
from openhcs.agent.dto.execution import (
ExecutionConnectionSpec,
RuntimeExecutionStatus,
RuntimeServerInfo,
RuntimeServerScanResult,
)
-from openhcs.agent.dto.ui_bridge import UiWindowSnapshotResult
+from formbridge.bridge.dto import UiWindowSnapshotResult
from openhcs.agent.dto.viewer import (
ViewerWindowDescriptor,
ViewerWindowImageSampleResult,
diff --git a/tests/unit/agent/test_mcp_distribution_runtime.py b/tests/unit/agent/test_mcp_distribution_runtime.py
index 40da11e2fc..0c73e8d11e 100644
--- a/tests/unit/agent/test_mcp_distribution_runtime.py
+++ b/tests/unit/agent/test_mcp_distribution_runtime.py
@@ -7,19 +7,19 @@
import time
from pathlib import Path
-from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.runtime_platform import (
+from formbridge.contracts import SCHEMA_VERSION
+from formbridge.runtime_platform import (
AgentRuntimePlatform,
LinuxRuntimePlatform,
WindowsRuntimePlatform,
)
-from openhcs.agent.services.ui_bridge_service import (
- UI_BRIDGE_PROTOCOL_VERSION,
- UiBridgeDescriptorDirectoryAuthority,
+from formbridge.bridge.service import (
+ UiBridgeDescriptorDirectories,
UiBridgeDescriptorReader,
)
from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
from python_introspect import to_jsonable
+from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
def _write_descriptor(path: Path) -> None:
@@ -27,7 +27,7 @@ def _write_descriptor(path: Path) -> None:
json.dumps(
{
"schema_version": SCHEMA_VERSION,
- "bridge_protocol_version": UI_BRIDGE_PROTOCOL_VERSION,
+ "bridge_protocol_version": OpenHCSUiBridgeApplication.protocol_version,
"application": to_jsonable(OPENHCS_ENDPOINT_APPLICATION),
"bridge_instance_id": "windows-test-bridge",
"pid": os.getpid(),
@@ -58,7 +58,7 @@ def test_windows_descriptor_default_uses_local_application_data(
staticmethod(lambda: WindowsRuntimePlatform),
)
- descriptor_dirs = UiBridgeDescriptorDirectoryAuthority.descriptor_dirs()
+ descriptor_dirs = UiBridgeDescriptorDirectories.descriptor_dirs()
assert descriptor_dirs[0] == (
local_application_data / "OpenHCS" / "runtime" / "ui-bridge"
diff --git a/tests/unit/agent/test_mcp_plate_stream_progress.py b/tests/unit/agent/test_mcp_plate_stream_progress.py
index 87289aa9c1..2dd6eb50a8 100644
--- a/tests/unit/agent/test_mcp_plate_stream_progress.py
+++ b/tests/unit/agent/test_mcp_plate_stream_progress.py
@@ -10,8 +10,7 @@
import time
import pytest
-from PyQt6.QtCore import QCoreApplication, QThread
-from pyqt_reactive.services.async_operation_executor import AsyncOperationExecutor
+from formbridge.services.async_operation_executor import AsyncOperationExecutor
from polystore.disk import DiskStorageBackend
from polystore.filemanager import FileManager
from polystore.roi import PointShape, ROI
@@ -64,7 +63,6 @@ def saved_roi_stream(tmp_path, monkeypatch):
def acquire(**fields):
assert threading.current_thread() is threading.main_thread()
- assert QThread.currentThread() == QCoreApplication.instance().thread()
assert fields["fresh"] is False and fields["ready_timeout"] == 30.0
return viewer
diff --git a/tests/unit/agent/test_mcp_process_resource_lifetime.py b/tests/unit/agent/test_mcp_process_resource_lifetime.py
index 1c0079f189..75158bdb86 100644
--- a/tests/unit/agent/test_mcp_process_resource_lifetime.py
+++ b/tests/unit/agent/test_mcp_process_resource_lifetime.py
@@ -207,7 +207,7 @@ def test_resident_connection_close_preserves_resource_until_process_owner_close(
monkeypatch, unix_socket_dir
):
from mcp.server.fastmcp import FastMCP
- from pyqt_reactive.services.async_operation_executor import AsyncOperationExecutor
+ from formbridge.services.async_operation_executor import AsyncOperationExecutor
from openhcs.mcp.dev_client_core import McpDevSocketSession
from openhcs.mcp.socket import McpSocketTransport, wait_for_socket
diff --git a/tests/unit/agent/test_mcp_server.py b/tests/unit/agent/test_mcp_server.py
index 0481e1e9b1..da094cfe01 100644
--- a/tests/unit/agent/test_mcp_server.py
+++ b/tests/unit/agent/test_mcp_server.py
@@ -14,7 +14,7 @@
from types import SimpleNamespace
import pytest
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from python_introspect import to_jsonable
from zmqruntime.client import EndpointShutdownResult
from zmqruntime.config import TransportMode
@@ -36,7 +36,8 @@
agent_capabilities,
get_capability_registry,
)
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentError
from openhcs.agent.dto.config import ConfigPatch
from openhcs.agent.dto.functions import (
CustomFunctionRegistrationResult,
@@ -54,7 +55,7 @@
PlatePathInspectionResult,
SyntheticPlateGenerationResult,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiActionCatalog,
UiActionIdentity,
UiActionSummary,
@@ -100,13 +101,9 @@
from openhcs.agent.services.object_state_field_help_service import (
ObjectStateFieldHelpService,
)
-from openhcs.agent.services.object_state_field_projection import (
- ObjectStateFieldListProjector,
-)
+from formbridge.bridge.field_projection import ObjectStateFieldListProjector
from openhcs.agent.services.selected_plate_service import SelectedPlateService
-from openhcs.agent.services.ui_bridge_service import (
- DEFAULT_UI_BRIDGE_CONNECTION_SPEC,
-)
+from formbridge.bridge.service import DEFAULT_UI_BRIDGE_CONNECTION_SPEC
from openhcs.agent.services.viewer_window_service import ViewerWindowService
from openhcs.core.streaming_config_declarations import ViewerType
from openhcs.mcp.context import OpenHCSAgentContext
@@ -271,15 +268,6 @@ def test_agent_dto_package_exports_mcp_debugging_contracts():
"UiSelectedPlateWorkflowKind",
"UiSelectedPlateWorkflowRequest",
"UiSelectedPlateWorkflowResult",
- "UiObjectStateFieldHelpRequest",
- "UiObjectStateFieldHelpResult",
- "UiObjectStateFieldMutationRequest",
- "UiObjectStateFieldMutationResult",
- "UiWidgetRect",
- "UiWidgetTreeNode",
- "UiWidgetTreeRequest",
- "UiWidgetTreeResult",
- "UiWidgetActionSummary",
"ViewerWindowLayerPayloads",
"ViewerWindowPayloadRecord",
"ViewerWindowPayloadRequest",
@@ -983,7 +971,7 @@ def test_mcp_tool_descriptions_expose_debugging_result_contracts():
def test_mcp_widget_tree_projection_preserves_required_empty_action_fields():
result = UiWidgetTreeResult(
schema_version=SCHEMA_VERSION,
- window_id="plate_manager",
+ window_id="datasets",
projected=True,
actionable_widgets=(
UiWidgetActionSummary(
@@ -1145,9 +1133,9 @@ def test_mcp_widget_tree_projection_preserves_semantic_action_values():
UiCodeDocumentSummary(
schema_version=SCHEMA_VERSION,
identity=UiCodeDocumentIdentity(
- document_id="plate_manager.orchestrator_config"
+ document_id="datasets.orchestrator_config"
),
- widget_id="plate_manager",
+ widget_id="datasets",
title="Plate config",
readable=True,
writable=True,
@@ -1164,9 +1152,9 @@ def test_mcp_widget_tree_projection_preserves_semantic_action_values():
UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
identity=UiStateSurfaceIdentity(
- surface_id="plate_manager.state"
+ surface_id="datasets.state"
),
- widget_id="plate_manager",
+ widget_id="datasets",
title="Plate state",
readable=True,
),
@@ -1182,7 +1170,7 @@ def test_mcp_widget_tree_projection_preserves_semantic_action_values():
UiActionSummary(
schema_version=SCHEMA_VERSION,
identity=UiActionIdentity(
- widget_id="plate_manager", action_id="add_datasets"
+ widget_id="datasets", action_id="add_datasets"
),
title="Add plate",
enabled=True,
@@ -1232,7 +1220,7 @@ def test_mcp_ui_catalog_invocations_preserve_declared_identity(
def test_mcp_widget_tree_projection_can_return_full_action_fields():
result = UiWidgetTreeResult(
schema_version=SCHEMA_VERSION,
- window_id="plate_manager",
+ window_id="datasets",
projected=True,
actionable_widgets=(
UiWidgetActionSummary(
@@ -1366,7 +1354,7 @@ async def call_widget_tree(arguments):
compact_result = asyncio.run(
call_widget_tree(
{
- "window_id": "plate_manager",
+ "window_id": "datasets",
"maximum_item_model_nodes": 256,
"include_tree": True,
"connection": {"timeout_ms": 1234},
@@ -1376,7 +1364,7 @@ async def call_widget_tree(arguments):
full_result = asyncio.run(
call_widget_tree(
{
- "window_id": "plate_manager",
+ "window_id": "datasets",
"compact_actions": False,
"connection": {"timeout_ms": 1234},
}
@@ -1393,7 +1381,7 @@ async def call_widget_tree(arguments):
assert full_payload["actionable_widgets"][0]["raw_value"] is None
assert ui_bridge_service.connections[0].timeout_ms == 1234
first_request = ui_bridge_service.widget_tree_requests[0]
- assert first_request.window_id == "plate_manager"
+ assert first_request.window_id == "datasets"
assert first_request.maximum_item_model_nodes == 256
assert first_request.include_tree is True
assert first_request.open_policy.create_if_missing is False
@@ -1621,7 +1609,7 @@ def test_mcp_stale_watchlist_includes_agent_contract_sources():
for path in watched_paths
)
assert any(
- path.endswith("openhcs/agent/dto/ui_bridge.py") for path in watched_paths
+ path.endswith("formbridge/bridge/dto.py") for path in watched_paths
)
assert any(path.endswith("openhcs/agent/dto/viewer.py") for path in watched_paths)
assert any(
@@ -1629,12 +1617,10 @@ def test_mcp_stale_watchlist_includes_agent_contract_sources():
for path in watched_paths
)
assert any(
- path.endswith("openhcs/agent/services/ui_bridge_service.py")
- for path in watched_paths
+ path.endswith("formbridge/bridge/service.py") for path in watched_paths
)
assert any(
- path.endswith("openhcs/agent/services/ui_bridge_transport.py")
- for path in watched_paths
+ path.endswith("formbridge/bridge/transport.py") for path in watched_paths
)
assert any(
path.endswith("openhcs/agent/services/viewer_window_service.py")
@@ -1648,7 +1634,7 @@ def test_mcp_stale_watchlist_includes_agent_contract_sources():
path.endswith("openhcs/runtime/viewer_protocol.py") for path in watched_paths
)
assert any(
- path.endswith("pyqt_reactive/services/window_snapshot.py")
+ path.endswith("formbridge/services/window_snapshot.py")
for path in watched_paths
)
@@ -2924,8 +2910,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
supported_selection_modes=("selected", "all"),
@@ -2985,7 +2971,7 @@ async def call_selected_plate_tool():
result = asyncio.run(call_selected_plate_tool())
payload = json.loads(_direct_tool_text(result))
- assert ui_bridge_service.state_surface_request.surface_id == "plate_manager.state"
+ assert ui_bridge_service.state_surface_request.surface_id == "datasets.state"
assert ui_bridge_service.state_surface_request.selection_mode == "selected"
assert ui_bridge_service.connection_fields.timeout_ms == 1234
assert plate_inspection_service.request.plate_path == selected_plate_root
@@ -3015,8 +3001,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
current_selection_count=1,
@@ -3113,8 +3099,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
supported_selection_modes=("selected", "all"),
@@ -3176,7 +3162,7 @@ async def call_selected_plate_query_tool():
result = asyncio.run(call_selected_plate_query_tool())
payload = json.loads(_direct_tool_text(result))
- assert ui_bridge_service.state_surface_request.surface_id == "plate_manager.state"
+ assert ui_bridge_service.state_surface_request.surface_id == "datasets.state"
assert ui_bridge_service.state_surface_request.selection_mode == "selected"
assert plate_inspection_service.request.plate_path == output_dataset_root
assert plate_inspection_service.request.source_format == "auto"
@@ -3228,8 +3214,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
current_selection_count=1,
@@ -3322,8 +3308,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
supported_selection_modes=("selected", "all"),
@@ -3418,7 +3404,7 @@ async def call_selected_plate_sample_tool():
result = asyncio.run(call_selected_plate_sample_tool())
payload = json.loads(_direct_tool_text(result))
- assert ui_bridge_service.state_surface_request.surface_id == "plate_manager.state"
+ assert ui_bridge_service.state_surface_request.surface_id == "datasets.state"
assert ui_bridge_service.state_surface_request.selection_mode == "selected"
assert plate_inspection_service.inspect_request is None
assert plate_inspection_service.sample_request.plate_path == selected_plate_root
@@ -3449,8 +3435,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
current_selection_count=1,
@@ -3558,8 +3544,8 @@ def get_state_surface(self, request, connection):
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
schema_version=SCHEMA_VERSION,
- identity=UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- widget_id="plate_manager",
+ identity=UiStateSurfaceIdentity(surface_id="datasets.state"),
+ widget_id="datasets",
title="Plate manager state",
readable=True,
current_selection_count=1,
@@ -4876,7 +4862,7 @@ def test_mcp_dev_client_ui_commands_project_connection_arguments():
state_args = parser.parse_args(
(
"state-surface",
- "plate_manager.state",
+ "datasets.state",
"--selection-mode",
"selected",
"--base-revision-token",
@@ -4891,7 +4877,7 @@ def test_mcp_dev_client_ui_commands_project_connection_arguments():
state_call = dev_client._calls_from_args(state_args)[0]
assert state_call.name == "openhcs_ui_get_state_surface"
- assert state_call.arguments["surface_id"] == "plate_manager.state"
+ assert state_call.arguments["surface_id"] == "datasets.state"
assert state_call.arguments["selection_mode"] == "selected"
assert state_call.arguments["base_revision_token"] == "rev-1"
assert state_call.arguments["connection"] == {
@@ -5643,7 +5629,7 @@ def _accepted_workflow_dev_result(
"action_result": {
"schema_version": "test",
"identity": {
- "widget_id": "plate_manager",
+ "widget_id": "datasets",
"action_id": workflow,
},
"status": "accepted",
@@ -5654,12 +5640,12 @@ def _accepted_workflow_dev_result(
},
"target_scope_ids": list(target_scope_ids),
"selection_revision_token": None,
- "workflow_status_surface_ids": ["plate_manager.state"],
+ "workflow_status_surface_ids": ["datasets.state"],
"event_sequence": 7,
"errors": [],
"warnings": [],
},
- "state_surface_id": "plate_manager.state",
+ "state_surface_id": "datasets.state",
"errors": [],
"warnings": [],
},
@@ -5752,7 +5738,7 @@ def _rejected_workflow_dev_result(
"action_result": {
"schema_version": "test",
"identity": {
- "widget_id": "plate_manager",
+ "widget_id": "datasets",
"action_id": "compile_datasets",
},
"status": "rejected",
@@ -5763,12 +5749,12 @@ def _rejected_workflow_dev_result(
},
"target_scope_ids": ["scope-a"],
"selection_revision_token": None,
- "workflow_status_surface_ids": ["plate_manager.state"],
+ "workflow_status_surface_ids": ["datasets.state"],
"event_sequence": 7,
"errors": [error],
"warnings": [],
},
- "state_surface_id": "plate_manager.state",
+ "state_surface_id": "datasets.state",
"errors": [error],
"warnings": [],
},
@@ -5804,7 +5790,7 @@ def test_mcp_dev_client_selected_workflow_poll_arguments_are_projected():
workflow_call = dev_client._calls_from_args(args)[0]
state_arguments = dev_client.state_surface_tool_arguments(
args,
- surface_id="plate_manager.state",
+ surface_id="datasets.state",
selection_mode=args.poll_selection_mode,
)
@@ -5825,7 +5811,7 @@ def test_mcp_dev_client_selected_workflow_poll_arguments_are_projected():
},
}
assert state_arguments == {
- "surface_id": "plate_manager.state",
+ "surface_id": "datasets.state",
"selection_mode": "all",
"base_revision_token": None,
"connection": {
@@ -5905,8 +5891,8 @@ def _complete_plate_manager_state(sparse):
from openhcs.agent.dto.ui_bridge import UiPlateManagerState
summary = UiStateSurfaceSummary(
- SCHEMA_VERSION, UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- "Plate Manager", True, widget_id="plate_manager",
+ SCHEMA_VERSION, UiStateSurfaceIdentity(surface_id="datasets.state"),
+ "Plate Manager", True, widget_id="datasets",
)
row = to_jsonable(DatasetRowState(
scope_id="scope", name="plate", root="/plate", pipeline_path=None,
@@ -5931,8 +5917,8 @@ def _state_surface_dev_wire(dev_client):
return to_jsonable(UiStateSurfaceDocument(
schema_version=SCHEMA_VERSION,
summary=UiStateSurfaceSummary(
- SCHEMA_VERSION, UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- "Plate Manager", True, widget_id="plate_manager",
+ SCHEMA_VERSION, UiStateSurfaceIdentity(surface_id="datasets.state"),
+ "Plate Manager", True, widget_id="datasets",
),
payload_schema="openhcs.ui.plate_manager_state.v1", payload={},
))
@@ -6175,7 +6161,7 @@ async def fake_call_tool(session, call, timeout_seconds):
"openhcs_ui_get_state_surface",
]
assert calls[0].arguments == {
- "surface_id": "plate_manager.state",
+ "surface_id": "datasets.state",
"selection_mode": "all",
"base_revision_token": None,
"connection": {
@@ -6527,7 +6513,7 @@ def test_mcp_dev_client_selected_workflow_poll_recovers_from_transient_read_time
import openhcs.mcp.dev_client as dev_client
import openhcs.mcp.dev_client_commands.ui as ui_commands
- from openhcs.agent.services.ui_bridge_service import UiBridgeGatewayTimeoutError
+ from formbridge.bridge.service import UiBridgeGatewayTimeoutError
calls: list[dev_client.McpDevToolCall] = []
state_call_count = 0
@@ -6638,7 +6624,7 @@ def test_mcp_dev_client_selected_workflow_poll_recovers_from_transient_baseline_
import openhcs.mcp.dev_client as dev_client
import openhcs.mcp.dev_client_commands.ui as ui_commands
- from openhcs.agent.services.ui_bridge_service import UiBridgeGatewayTimeoutError
+ from formbridge.bridge.service import UiBridgeGatewayTimeoutError
workflow_call_count = 0
state_call_count = 0
@@ -6746,7 +6732,7 @@ def test_mcp_dev_client_selected_workflow_poll_exhausts_transient_read_timeout(
import openhcs.mcp.dev_client as dev_client
import openhcs.mcp.dev_client_commands.ui as ui_commands
- from openhcs.agent.services.ui_bridge_service import UiBridgeGatewayTimeoutError
+ from formbridge.bridge.service import UiBridgeGatewayTimeoutError
workflow_call_count = 0
state_call_count = 0
@@ -7026,14 +7012,16 @@ def test_mcp_dev_client_selected_workflow_poll_summarizes_rejection(
import openhcs.mcp.dev_client as dev_client
import openhcs.mcp.dev_client_commands.ui as ui_commands
- from openhcs.agent.dto.ui_bridge import (
- UiPlateManagerState, UiStateSurfaceDocument,
- UiStateSurfaceIdentity, UiStateSurfaceSummary,
+ from openhcs.agent.dto.ui_bridge import UiPlateManagerState
+ from formbridge.bridge.dto import (
+ UiStateSurfaceDocument,
+ UiStateSurfaceIdentity,
+ UiStateSurfaceSummary,
)
surface_summary = UiStateSurfaceSummary(
- "test", UiStateSurfaceIdentity(surface_id="plate_manager.state"),
- "Plate Manager", True, widget_id="plate_manager",
+ "test", UiStateSurfaceIdentity(surface_id="datasets.state"),
+ "Plate Manager", True, widget_id="datasets",
)
baseline = UiPlateManagerState(
schema_version="test", summary=surface_summary, object_state_token=1,
@@ -8485,8 +8473,8 @@ async def call_health_through_dev_client():
def test_mcp_dev_client_server_spec_preserves_gui_session_environment(monkeypatch):
import openhcs.mcp.dev_client as dev_client
from openhcs.agent.path_policy import AgentPathPolicy
- from openhcs.agent.runtime_platform import AgentRuntimePlatform
- from openhcs.agent.ui_bridge_environment import UiBridgeDescriptorEnvironment
+ from formbridge.runtime_platform import AgentRuntimePlatform
+ from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
from openhcs.core.native_threading import native_thread_count_environment_keys
from openhcs.utils.environment import OpenHCSProcessEnvironment
@@ -8495,7 +8483,7 @@ def test_mcp_dev_client_server_spec_preserves_gui_session_environment(monkeypatc
monkeypatch.delenv(key, raising=False)
for key in AgentPathPolicy.environment_keys():
monkeypatch.delenv(key, raising=False)
- for key in UiBridgeDescriptorEnvironment.child_process_environment_keys():
+ for key in OpenHCSUiBridgeApplication.descriptor_environment_keys():
monkeypatch.setenv(key, f"/tmp/{key.casefold()}")
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.setenv("XAUTHORITY", "/tmp/test.Xauthority")
@@ -8540,10 +8528,10 @@ def test_mcp_dev_client_server_spec_preserves_gui_session_environment(monkeypatc
assert environment[bootstrap.MCP_VERBOSE_ENVIRONMENT_VARIABLE] == "1"
assert {
key: environment[key]
- for key in UiBridgeDescriptorEnvironment.child_process_environment_keys()
+ for key in OpenHCSUiBridgeApplication.descriptor_environment_keys()
} == {
key: f"/tmp/{key.casefold()}"
- for key in UiBridgeDescriptorEnvironment.child_process_environment_keys()
+ for key in OpenHCSUiBridgeApplication.descriptor_environment_keys()
}
assert {
key: environment[key] for key in native_thread_count_environment_keys()
@@ -8560,10 +8548,7 @@ def test_mcp_dev_client_server_spec_preserves_gui_session_environment(monkeypatc
def test_mcp_dev_client_server_spec_preserves_windows_host_environment(monkeypatch):
import openhcs.mcp.dev_client as dev_client
- from openhcs.agent.runtime_platform import (
- AgentRuntimePlatform,
- WindowsRuntimePlatform,
- )
+ from formbridge.runtime_platform import AgentRuntimePlatform, WindowsRuntimePlatform
windows_authority = WindowsRuntimePlatform()
expected_keys = {
diff --git a/tests/unit/agent/test_mcp_skill_registration.py b/tests/unit/agent/test_mcp_skill_registration.py
index eee330b598..2a478171b1 100644
--- a/tests/unit/agent/test_mcp_skill_registration.py
+++ b/tests/unit/agent/test_mcp_skill_registration.py
@@ -7,7 +7,7 @@
import pytest
from openhcs.agent import skill_sync
-from openhcs.agent.runtime_platform import LinuxRuntimePlatform
+from formbridge.runtime_platform import LinuxRuntimePlatform
from openhcs.agent.skill_bundle import AgentSkillBundle
from openhcs.agent.skill_sync import SkillSyncReceipt, sync_skills
from openhcs.mcp import client_registration
diff --git a/tests/unit/agent/test_object_state_field_help_service.py b/tests/unit/agent/test_object_state_field_help_service.py
index d13faedb8e..90285be7d1 100644
--- a/tests/unit/agent/test_object_state_field_help_service.py
+++ b/tests/unit/agent/test_object_state_field_help_service.py
@@ -11,7 +11,7 @@
SuppliedFromArtifactInput,
SuppliedFromPrimaryInput,
)
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiObjectStateFieldHelpRequest,
UiObjectStateFieldSummary,
UiObjectStateScopeCatalog,
@@ -19,9 +19,7 @@
UiObjectStateScopeSummary,
UiSemanticAddress,
)
-from openhcs.agent.services.ui_bridge_service import (
- UiBridgeListObjectStateScopesOperation,
-)
+from formbridge.bridge.service import UiBridgeListObjectStateScopesOperation
from openhcs.agent.services.object_state_field_help_service import (
ObjectStateFieldHelpService,
)
diff --git a/tests/unit/agent/test_owned_runtime_bootstrap.py b/tests/unit/agent/test_owned_runtime_bootstrap.py
index 6faed35b1a..6b9be0297e 100644
--- a/tests/unit/agent/test_owned_runtime_bootstrap.py
+++ b/tests/unit/agent/test_owned_runtime_bootstrap.py
@@ -31,6 +31,7 @@
EndpointShutdownMode,
EndpointShutdownResult,
)
+from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG
_PRODUCTION_SPAWN = ZMQExecutionClient._spawn_server_process
@@ -279,7 +280,7 @@ def test_effective_connection_matches_configured_native_endpoint(
configured_mode if requested_mode is None else requested_mode
)
resolved = connection.resolved(config)
- assert resolved.transport_endpoint() == endpoint
+ assert resolved.transport_endpoint(OPENHCS_ZMQ_CONFIG) == endpoint
assert resolved.execution_client(config).endpoint == endpoint
assert connection.transport_mode is requested_mode # Input stays immutable.
assert ZMQExecutionClient(config=config).endpoint == config.client_endpoint()
@@ -347,7 +348,7 @@ def close(client, identity, *, mode, operation_deadline):
started = service.start_from_request(request)
handle = dataclass_from_mapping(RuntimeBootstrapHandle, to_jsonable(started.handle))
assert handle.connection.transport_mode is expected.transport_mode
- assert handle.connection.transport_endpoint() == expected
+ assert handle.connection.transport_endpoint(OPENHCS_ZMQ_CONFIG) == expected
assert not handle.launch_plan.startup_status_file.exists()
# Retained explicit routing wins even if another observer has a different
# configured default. No second transport record or mutable mode cache.
diff --git a/tests/unit/agent/test_plate_inspection_service.py b/tests/unit/agent/test_plate_inspection_service.py
index 66cb9b206f..0cf9ad7a4a 100644
--- a/tests/unit/agent/test_plate_inspection_service.py
+++ b/tests/unit/agent/test_plate_inspection_service.py
@@ -264,7 +264,7 @@ def test_plate_inspection_auto_detects_imagexpress_without_mutating(tmp_path: Pa
is PlateInspectionSourceBindingRole.NOT_PROJECTED_BY_HANDLER
)
assert result.workflow_advice.ui_code_document_id == (
- "plate_manager.orchestrator_config"
+ "datasets.orchestrator_config"
)
assert result.workflow_advice.ui_operation == "initialize_datasets"
assert not (plate / "openhcs_metadata.json").exists()
diff --git a/tests/unit/agent/test_s1_typed_viewer_rendering.py b/tests/unit/agent/test_s1_typed_viewer_rendering.py
index db34f1d168..8b1292a751 100644
--- a/tests/unit/agent/test_s1_typed_viewer_rendering.py
+++ b/tests/unit/agent/test_s1_typed_viewer_rendering.py
@@ -11,7 +11,8 @@
ProbeViewerWindowCapability,
ValidateViewerWindowStateCapability,
)
-from openhcs.agent.dto.common import AgentError, AgentWarning, SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentWarning
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.execution import ExecutionConnectionSpec
from openhcs.agent.dto.viewer import (
ViewerWindowDescriptor,
diff --git a/tests/unit/agent/test_ui_bridge_operation_family.py b/tests/unit/agent/test_ui_bridge_operation_family.py
index c1b3b75623..1bd3cc966f 100644
--- a/tests/unit/agent/test_ui_bridge_operation_family.py
+++ b/tests/unit/agent/test_ui_bridge_operation_family.py
@@ -16,15 +16,25 @@
UiBridgeCapability,
agent_capability_declarations,
)
-from openhcs.agent.services.ui_bridge_service import UiBridgeOperation
+from formbridge.bridge.service import UiBridgeOperation
REPOSITORY = Path(__file__).resolve().parents[3]
PRODUCTION = REPOSITORY / "openhcs"
+# The generic bridge lives in formbridge (Qt-free) and pyqt-reactive (Qt).
+BRIDGE_SOURCES = (
+ PRODUCTION,
+ REPOSITORY / "external" / "formbridge" / "src" / "formbridge",
+ REPOSITORY / "external" / "pyqt-reactive" / "src" / "pyqt_reactive",
+)
+
+
+def _sources() -> tuple[Path, ...]:
+ return tuple(path for root in BRIDGE_SOURCES for path in root.rglob("*.py"))
def _classes() -> dict[str, tuple[Path, ast.ClassDef]]:
classes = {}
- for path in PRODUCTION.rglob("*.py"):
+ for path in _sources():
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
if isinstance(node, ast.ClassDef):
classes[node.name] = (path, node)
@@ -97,10 +107,10 @@ def test_service_defines_no_per_operation_method() -> None:
def test_every_wire_operation_has_one_running_ui_implementation() -> None:
- from openhcs.pyqt_gui.services.ui_agent_bridge import UiAgentBridgeService
+ from openhcs.pyqt_gui.services.ui_agent_bridge import OpenHCSUiAgentBridgeService
wire = set(UiBridgeOperation.__registry__.values())
- assert set(UiAgentBridgeService.served_operations) == wire
+ assert set(OpenHCSUiAgentBridgeService.served_operations) == wire
def test_every_operation_answers_its_own_failure() -> None:
@@ -171,7 +181,7 @@ def test_deleted_bridge_mechanisms_stay_deleted() -> None:
)
offenders = [
f"{path.relative_to(REPOSITORY)}:{number}"
- for path in PRODUCTION.rglob("*.py")
+ for path in _sources()
for number, line in enumerate(
path.read_text(encoding="utf-8").splitlines(), start=1
)
diff --git a/tests/unit/agent/test_ui_bridge_service.py b/tests/unit/agent/test_ui_bridge_service.py
index ea5db8b110..304da17fb9 100644
--- a/tests/unit/agent/test_ui_bridge_service.py
+++ b/tests/unit/agent/test_ui_bridge_service.py
@@ -1,5 +1,5 @@
from __future__ import annotations
-from openhcs.core.selection import AllItems, SelectedItems
+from formbridge.selection import AllItems, SelectedItems
from openhcs.agent.dto.session import DatasetRowState
import inspect
@@ -10,12 +10,13 @@
from pathlib import Path
import pytest
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from zmqruntime import EndpointApplication, EndpointApplicationCompatibilityError
from openhcs.agent.path_policy import AgentPathPolicy
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError, AgentResourceRef
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.contracts import SCHEMA_VERSION
+from formbridge.contracts import AgentError, AgentResourceRef
+from formbridge.bridge.dto import (
ActionAccepted,
OperationCompleted,
OperationFailed,
@@ -43,7 +44,6 @@
UiCodeDocumentApplyRequest,
UiCodeDocumentApplyResult,
UiCodeDocumentCatalog,
- UiCodeDocumentId,
UiCodeDocumentIdentity,
UiCodeDocumentRequest,
UiCodeDocumentSummary,
@@ -60,10 +60,6 @@
UiObjectStateScopeListRequest,
UiObjectStateScopeSummary,
UiObjectStateValuePreview,
- UiPlateManagerState,
- UiSelectedPlateWorkflowKind,
- UiSelectedPlateWorkflowRequest,
- UiSelectedPlateWorkflowResult,
UiSemanticAddress,
UiSnapshotCatalog,
UiSnapshotListRequest,
@@ -72,7 +68,6 @@
UiSnapshotRestoreResult,
UiStateSurfaceCatalog,
UiStateSurfaceDocument,
- UiStateSurfaceId,
UiStateSurfaceIdentity,
UiStateSurfaceRequest,
UiStateSurfaceSummary,
@@ -97,12 +92,17 @@
UiWindowSnapshotResult,
UiWindowSummary,
)
-from openhcs.agent.runtime_platform import AgentRuntimePlatform
-from openhcs.agent.services.ui_bridge_service import (
- UI_BRIDGE_PROTOCOL_VERSION,
+from openhcs.agent.dto.ui_bridge import (
+ UiPlateManagerState,
+ UiSelectedPlateWorkflowKind,
+ UiSelectedPlateWorkflowRequest,
+ UiSelectedPlateWorkflowResult,
+)
+from formbridge.runtime_platform import AgentRuntimePlatform
+from formbridge.bridge.service import (
UiBridgeApplyDocumentOperation,
UiBridgeCloseWindowOperation,
- UiBridgeDescriptorDirectoryAuthority,
+ UiBridgeDescriptorDirectories,
UiBridgeDescriptorReader,
UiBridgeGatewayABC,
UiBridgeGatewayResponseError,
@@ -117,7 +117,6 @@
UiBridgeMutateObjectStateFieldOperation,
UiBridgeProcessAdvertisedDescriptorCatalog,
UiBridgeRestoreSnapshotOperation,
- UiBridgeSelectedPlateWorkflowOperation,
UiBridgeService,
UiBridgeSnapshotWindowOperation,
UiBridgeStatusOperation,
@@ -125,14 +124,21 @@
UiBridgeWaitForOperationReceiptOperation,
UiBridgeWidgetTreeOperation,
)
-from openhcs.agent.services.ui_bridge_transport import UiBridgeControlClient
+from openhcs.agent.services.ui_bridge_service import UiBridgeSelectedPlateWorkflowOperation
+from formbridge.bridge.transport import UiBridgeControlClient
from openhcs.runtime.viewer_protocol import HeadlessSession, ProjectedGraphicalSession
from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
from python_introspect import to_jsonable
+from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
+from openhcs.agent.ui_bridge_identities import (
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+)
+from openhcs.agent.services.ui_bridge_service import OpenHCSUiBridgeService
-DOCUMENT_ID = UiCodeDocumentId.PLATE_MANAGER_ORCHESTRATOR.value
-STATE_SURFACE_ID = UiStateSurfaceId.PLATE_MANAGER.value
-WIDGET_ID = "plate_manager"
+DOCUMENT_ID = DatasetListCodeDocumentIdentity.require_value()
+STATE_SURFACE_ID = DatasetListStateSurfaceIdentity.require_value()
+WIDGET_ID = "datasets"
PLATE_SCOPE_ID = "plate-1"
PLATE_NAME = "plate 1"
BRIDGE_ID = "bridge-1"
@@ -170,7 +176,7 @@ class UiBridgeDescriptorFile:
def write(self) -> Path:
payload = {
"schema_version": SCHEMA_VERSION,
- "bridge_protocol_version": UI_BRIDGE_PROTOCOL_VERSION,
+ "bridge_protocol_version": OpenHCSUiBridgeApplication.protocol_version,
"application": to_jsonable(OPENHCS_ENDPOINT_APPLICATION),
"bridge_instance_id": self.bridge_id,
"pid": os.getpid(),
@@ -885,7 +891,7 @@ def test_ui_bridge_service_resolves_projected_graphical_viewer_launch_context(
monkeypatch.setenv("OPENHCS_UI_BRIDGE_DESCRIPTOR", str(descriptor))
monkeypatch.setattr(
- "openhcs.agent.runtime_platform."
+ "formbridge.runtime_platform."
"LinuxRuntimePlatform._process_environment",
lambda self, pid: (
{
@@ -900,7 +906,7 @@ def test_ui_bridge_service_resolves_projected_graphical_viewer_launch_context(
),
)
- launch_context = UiBridgeService(path_policy=object()).viewer_launch_context()
+ launch_context = OpenHCSUiBridgeService(path_policy=object()).viewer_launch_context()
assert launch_context.session is ProjectedGraphicalSession
assert launch_context.environment_overlay == {
@@ -993,7 +999,7 @@ def test_descriptor_reader_rejects_mismatched_openhcs_application(
def test_ui_bridge_client_rejects_mismatched_server_application() -> None:
request = UiBridgeRequestEnvelope(
schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
+ bridge_protocol_version=OpenHCSUiBridgeApplication.protocol_version,
application=OPENHCS_ENDPOINT_APPLICATION,
request_id="request-1",
operation="status",
@@ -1002,7 +1008,7 @@ def test_ui_bridge_client_rejects_mismatched_server_application() -> None:
)
response = UiBridgeResponseEnvelope(
schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
+ bridge_protocol_version=OpenHCSUiBridgeApplication.protocol_version,
application=EndpointApplication(identifier="openhcs", version="0.7.22"),
request_id=request.request_id,
ok=True,
@@ -1050,7 +1056,7 @@ def test_descriptor_catalog_removes_dead_process_before_application_compatibilit
descriptor_path.write_text(json.dumps(payload), encoding="utf-8")
monkeypatch.delenv("OPENHCS_UI_BRIDGE_DESCRIPTOR", raising=False)
monkeypatch.setattr(
- UiBridgeDescriptorDirectoryAuthority,
+ UiBridgeDescriptorDirectories,
"descriptor_dirs",
classmethod(lambda cls: (descriptor_directory,)),
)
@@ -1168,7 +1174,7 @@ def test_descriptor_resolver_reports_ambiguous_live_descriptors(monkeypatch, tmp
monkeypatch.delenv("DISPLAY", raising=False)
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
- service = UiBridgeService()
+ service = OpenHCSUiBridgeService()
status = service.invoke(UiBridgeStatusOperation)
launch_context = service.viewer_launch_context()
@@ -1237,7 +1243,7 @@ def test_descriptor_resolution_uses_process_advertised_descriptor(
proc_root,
)
monkeypatch.setattr(
- UiBridgeDescriptorDirectoryAuthority,
+ UiBridgeDescriptorDirectories,
"descriptor_dirs",
classmethod(lambda cls: (tmp_path / "empty-descriptor-dir",)),
)
@@ -1303,7 +1309,7 @@ def test_descriptor_catalog_unions_default_and_process_advertised_live_bridges(
).write()
monkeypatch.delenv("OPENHCS_UI_BRIDGE_DESCRIPTOR", raising=False)
monkeypatch.setattr(
- UiBridgeDescriptorDirectoryAuthority,
+ UiBridgeDescriptorDirectories,
"descriptor_dirs",
classmethod(lambda cls: (default_directory,)),
)
@@ -1343,7 +1349,7 @@ def test_descriptor_catalog_reads_duplicate_resolved_path_once(monkeypatch, tmp_
alias_path = descriptor_directory / ".." / "descriptors" / descriptor_path.name
monkeypatch.delenv("OPENHCS_UI_BRIDGE_DESCRIPTOR", raising=False)
monkeypatch.setattr(
- UiBridgeDescriptorDirectoryAuthority,
+ UiBridgeDescriptorDirectories,
"descriptor_dirs",
classmethod(lambda cls: (descriptor_directory,)),
)
@@ -1877,7 +1883,7 @@ def close_window(
assert error.code == "unsupported_ui_bridge_operation"
assert error.hint is not None
assert "Restart the UI or UI bridge process" in error.hint
- assert "current OpenHCS source" in error.hint
+ assert "current source" in error.hint
def test_restore_request_rejects_multiple_selectors(monkeypatch, tmp_path):
@@ -1983,7 +1989,7 @@ def get_operation_status(connection, request):
monkeypatch.setattr(gateway, "get_operation_status", get_operation_status)
monkeypatch.setattr(
- "openhcs.agent.services.ui_bridge_service.time.sleep",
+ "formbridge.bridge.service.time.sleep",
lambda _seconds: None,
)
service = UiBridgeService(gateway=gateway)
@@ -2031,7 +2037,7 @@ def test_wait_for_operation_receipt_returns_fresh_running_ref_with_timeout_error
lambda _connection, _request: running,
)
monkeypatch.setattr(
- "openhcs.agent.services.ui_bridge_service.time.monotonic",
+ "formbridge.bridge.service.time.monotonic",
lambda: next(monotonic_values),
)
service = UiBridgeService(gateway=gateway)
@@ -2050,7 +2056,7 @@ def test_wait_for_operation_receipt_returns_fresh_running_ref_with_timeout_error
assert result.status == "running"
assert result.started_at_unix == 7.0
assert result.errors[0].code == "ui_bridge_operation_wait_timeout"
- assert "openhcs_ui_wait_for_operation_receipt" in result.errors[0].hint
+ assert "get_operation_status" in result.errors[0].hint
assert "does not establish domain workflow completion" in result.errors[0].hint
assert result.identity is running.identity
@@ -2148,7 +2154,7 @@ def test_ui_requests_own_mcp_tool_argument_projection():
base_revision_token="rev-1",
),
UiActionInvokeRequest.from_fields(
- widget_id="plate_manager",
+ widget_id="datasets",
action_id="compile_datasets",
),
UiWidgetActionInvokeRequest.from_fields(
diff --git a/tests/unit/agent/test_user_facing_help_projection.py b/tests/unit/agent/test_user_facing_help_projection.py
index 7804584e2e..16361297c8 100644
--- a/tests/unit/agent/test_user_facing_help_projection.py
+++ b/tests/unit/agent/test_user_facing_help_projection.py
@@ -7,7 +7,7 @@
from pathlib import Path
from objectstate import ObjectState
-from pyqt_reactive.services.parameter_help_service import (
+from formbridge.services.parameter_help_service import (
dataclass_parameter_descriptions,
docstring_info_for_target,
)
@@ -527,6 +527,7 @@ def test_help_projection_has_no_semantic_mirror_or_name_dispatch() -> None:
production_roots = (
REPOSITORY_ROOT / "openhcs",
REPOSITORY_ROOT / "external/arraybridge/src",
+ REPOSITORY_ROOT / "external/formbridge/src",
REPOSITORY_ROOT / "external/pyqt-reactive/src",
REPOSITORY_ROOT / "external/python-introspect/src",
REPOSITORY_ROOT / "scripts",
diff --git a/tests/unit/agent/test_viewer_render_snapshot.py b/tests/unit/agent/test_viewer_render_snapshot.py
index 64192eac87..5c03d086f9 100644
--- a/tests/unit/agent/test_viewer_render_snapshot.py
+++ b/tests/unit/agent/test_viewer_render_snapshot.py
@@ -9,15 +9,17 @@
import pytest
from pyqt_reactive.services.window_snapshot import (
QtWindowSnapshotRequest,
+ WindowSnapshotRenderOwner,
+)
+from formbridge.services.window_snapshot import (
WindowRenderFrame,
WindowSnapshotFrameCondition,
- WindowSnapshotRenderOwner,
WindowVisualObservation,
)
from python_introspect import dataclass_from_mapping, to_jsonable
from openhcs.agent.capabilities import ViewerSnapshotWindowCapability
-from openhcs.agent.dto.common import AgentError
+from formbridge.contracts import AgentError
from openhcs.agent.dto.execution import ExecutionConnectionSpec
from openhcs.agent.dto.viewer import (
ViewerWindowDescriptor,
diff --git a/tests/unit/benchmark/test_agent_validation_attempt_recorder.py b/tests/unit/benchmark/test_agent_validation_attempt_recorder.py
index 77037adc67..db758d4843 100644
--- a/tests/unit/benchmark/test_agent_validation_attempt_recorder.py
+++ b/tests/unit/benchmark/test_agent_validation_attempt_recorder.py
@@ -31,11 +31,7 @@
DatasetRowState,
SessionOperationResult,
)
-from openhcs.agent.dto.ui_bridge import (
- UiCodeDocument,
- UiCodeDocumentIdentity,
- UiCodeDocumentSummary,
-)
+from formbridge.bridge.dto import UiCodeDocument, UiCodeDocumentIdentity, UiCodeDocumentSummary
from openhcs.core.config import PipelineConfig
from openhcs.core.pipeline_document import PipelineDocumentCodec
from openhcs.core.steps.function_step import FunctionStep
diff --git a/tests/unit/desktop/test_desktop_deployment.py b/tests/unit/desktop/test_desktop_deployment.py
index ab041789b8..e2dd14ff99 100644
--- a/tests/unit/desktop/test_desktop_deployment.py
+++ b/tests/unit/desktop/test_desktop_deployment.py
@@ -11,10 +11,7 @@
import pytest
from openhcs.desktop import deployment as desktop_deployment
-from openhcs.agent.runtime_platform import (
- MacOSRuntimePlatform,
- WindowsRuntimePlatform,
-)
+from formbridge.runtime_platform import MacOSRuntimePlatform, WindowsRuntimePlatform
from openhcs.desktop.deployment import (
DesktopDeployment,
DesktopDeploymentContext,
diff --git a/tests/unit/desktop/test_desktop_update.py b/tests/unit/desktop/test_desktop_update.py
index 1a6f80d1a2..52a02b4bc8 100644
--- a/tests/unit/desktop/test_desktop_update.py
+++ b/tests/unit/desktop/test_desktop_update.py
@@ -14,7 +14,7 @@
BackgroundProcessPlatform,
WindowsBackgroundProcesses,
)
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.scope_token_service import ScopeTokenService
from pyqt_reactive.theming import ColorScheme
import openhcs.pyqt_gui.main as main_module
diff --git a/tests/unit/desktop/test_desktop_update_worker.py b/tests/unit/desktop/test_desktop_update_worker.py
index 3d252f5ea9..31b4a7501a 100644
--- a/tests/unit/desktop/test_desktop_update_worker.py
+++ b/tests/unit/desktop/test_desktop_update_worker.py
@@ -960,6 +960,7 @@ def test_worker_process_waits_updates_restarts_and_restores_session(
"external/arraybridge/src",
"external/pycodify/src",
"external/PolyStore/src",
+ "external/formbridge/src",
"external/pyqt-reactive/src",
"external/zmqruntime/src",
)
diff --git a/tests/unit/desktop/test_zmq_version_restart.py b/tests/unit/desktop/test_zmq_version_restart.py
index 780df5766f..8e0b300e4a 100644
--- a/tests/unit/desktop/test_zmq_version_restart.py
+++ b/tests/unit/desktop/test_zmq_version_restart.py
@@ -6,9 +6,7 @@
from types import SimpleNamespace
from PyQt6.QtWidgets import QDialog
-from pyqt_reactive.services.async_operation_executor import (
- AsyncOperationExecutorClosedError,
-)
+from formbridge.services.async_operation_executor import AsyncOperationExecutorClosedError
from openhcs.core.execution_state import ManagerExecutionState
from openhcs.desktop.update import DesktopUpdateError
diff --git a/tests/unit/pyqt_gui/test_cellprofiler_parameter_help.py b/tests/unit/pyqt_gui/test_cellprofiler_parameter_help.py
index 415bc3f21b..5d7145902c 100644
--- a/tests/unit/pyqt_gui/test_cellprofiler_parameter_help.py
+++ b/tests/unit/pyqt_gui/test_cellprofiler_parameter_help.py
@@ -14,7 +14,7 @@ def test_parameter_help_content_uses_parameter_window_not_docstring_mirror(
import openhcs # noqa: F401 - activates source-checkout externals first
from PyQt6.QtWidgets import QApplication
- from pyqt_reactive.services.help_document import HelpDocumentFormat
+ from formbridge.services.help_document import HelpDocumentFormat
from pyqt_reactive.widgets.help_document_browser import HelpDocumentBrowser
from pyqt_reactive.windows.help_window_manager import (
HELP_WINDOW_DIALOG_MARGIN,
@@ -146,9 +146,7 @@ def enableable_probe(image, *, enabled: bool = True):
def test_callable_popup_projection_has_no_parameter_name_dispatch() -> None:
import openhcs # noqa: F401 - activates source-checkout externals first
- from pyqt_reactive.services.parameter_help_service import (
- parameter_description_from_target,
- )
+ from formbridge.services.parameter_help_service import parameter_description_from_target
source = inspect.getsource(parameter_description_from_target)
tree = ast.parse(source)
@@ -249,7 +247,7 @@ def record_docstring_help(*args, **kwargs):
def test_dataclass_help_uses_source_field_docs_instead_of_signature() -> None:
import openhcs # noqa: F401 - activates source-checkout externals first
from openhcs.core.config import LazyPathPlanningConfig, PathPlanningConfig
- from pyqt_reactive.services.parameter_help_service import (
+ from formbridge.services.parameter_help_service import (
docstring_info_for_target,
resolved_parameter_description,
)
@@ -293,7 +291,7 @@ def test_nested_dataclass_form_uses_nested_dataclass_help_target(
ParameterFormManager,
)
from pyqt_reactive.theming import ColorScheme
- from pyqt_reactive.services.parameter_help_service import source_dataclass_type
+ from formbridge.services.parameter_help_service import source_dataclass_type
app = QApplication.instance() or QApplication([])
manager = ParameterFormManager(
diff --git a/tests/unit/pyqt_gui/test_code_document_catalog_imports.py b/tests/unit/pyqt_gui/test_code_document_catalog_imports.py
index 6cf90472f1..2131918200 100644
--- a/tests/unit/pyqt_gui/test_code_document_catalog_imports.py
+++ b/tests/unit/pyqt_gui/test_code_document_catalog_imports.py
@@ -38,8 +38,8 @@
from openhcs.pyqt_gui.services.ui_agent_bridge import (
UiCodeDocumentExecutionService,
UiCodeDocumentSourcePolicy,
- UiCodeDocumentValidationError,
)
+from formbridge.bridge.in_process import UiCodeDocumentValidationError
from openhcs.ui.shared.plate_manager_code_document import (
PlateManagerCodeDocumentAuthority,
)
diff --git a/tests/unit/pyqt_gui/test_config_preview_formatters.py b/tests/unit/pyqt_gui/test_config_preview_formatters.py
index 7fc0e83e6c..4a5d4612f2 100644
--- a/tests/unit/pyqt_gui/test_config_preview_formatters.py
+++ b/tests/unit/pyqt_gui/test_config_preview_formatters.py
@@ -1,6 +1,6 @@
"""Declaration-driven OpenHCS config preview projections."""
-from pyqt_reactive.utils.preview_formatters import resolve_preview_label
+from formbridge.utils.preview_formatters import resolve_preview_label
from openhcs.core.config import (
LazyNapariStreamingConfig,
diff --git a/tests/unit/pyqt_gui/test_dual_editor_window_artifact_refresh.py b/tests/unit/pyqt_gui/test_dual_editor_window_artifact_refresh.py
index 2919cb0ef2..88e80d7c5f 100644
--- a/tests/unit/pyqt_gui/test_dual_editor_window_artifact_refresh.py
+++ b/tests/unit/pyqt_gui/test_dual_editor_window_artifact_refresh.py
@@ -4,7 +4,7 @@
from openhcs.core.steps.function_step import FunctionStep
from openhcs.pyqt_gui.windows.dual_editor_window import DualEditorWindow
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.scope_token_service import ScopeTokenService
from pyqt_reactive.widgets.shared.base_form_dialog import BaseFormDialog
from pyqt_reactive.widgets.shared.dirty_window_presenter import DirtyWindowStateTracker
diff --git a/tests/unit/pyqt_gui/test_durable_decorated_history.py b/tests/unit/pyqt_gui/test_durable_decorated_history.py
index b6af34a318..a64b398dbd 100644
--- a/tests/unit/pyqt_gui/test_durable_decorated_history.py
+++ b/tests/unit/pyqt_gui/test_durable_decorated_history.py
@@ -11,7 +11,7 @@
from arraybridge.decorators import ThreadGPUContext
from arraybridge.types import MemoryType
from objectstate import ObjectStateRegistry
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.scope_token_service import ScopeTokenService
from openhcs.core.config import PipelineConfig
from openhcs.core.memory import numpy
diff --git a/tests/unit/pyqt_gui/test_generic_qt_machinery_guards.py b/tests/unit/pyqt_gui/test_generic_qt_machinery_guards.py
index 7839ea6355..3fd20dc1cf 100644
--- a/tests/unit/pyqt_gui/test_generic_qt_machinery_guards.py
+++ b/tests/unit/pyqt_gui/test_generic_qt_machinery_guards.py
@@ -12,6 +12,7 @@
PRODUCT = REPO / "openhcs"
PYQT_REACTIVE = REPO / "external" / "pyqt-reactive" / "src" / "pyqt_reactive"
OBJECTSTATE = REPO / "external" / "ObjectState" / "src" / "objectstate"
+FORMBRIDGE = REPO / "external" / "formbridge" / "src" / "formbridge"
MOVED_MODULES = (
"core/path_cache.py",
@@ -103,7 +104,9 @@ def test_openhcs_defines_no_library_machinery() -> None:
assert found == []
-@pytest.mark.parametrize("root", (PRODUCT, PYQT_REACTIVE, OBJECTSTATE), ids=lambda p: p.name)
+@pytest.mark.parametrize(
+ "root", (PRODUCT, PYQT_REACTIVE, FORMBRIDGE, OBJECTSTATE), ids=lambda p: p.name
+)
def test_scope_ids_are_built_through_scope_key(root: Path) -> None:
"""No hand-written '::' scope grammar outside objectstate.scope_key."""
@@ -138,7 +141,7 @@ def test_xdg_paths_carry_no_legacy_migration() -> None:
def test_library_families_are_classes_not_enums() -> None:
- import pyqt_reactive.core.path_cache as path_cache
+ import formbridge.core.path_cache as path_cache
import pyqt_reactive.services.time_travel_navigation as time_travel_navigation
import pyqt_reactive.widgets.enhanced_path_widget as enhanced_path_widget
import pyqt_reactive.widgets.grid_selector as grid_selector
@@ -155,7 +158,11 @@ def test_library_families_are_classes_not_enums() -> None:
def test_external_editor_switch_is_named_once() -> None:
readers = sorted(
str(path.relative_to(REPO))
- for path in (*_python_files(PRODUCT), *_python_files(PYQT_REACTIVE))
+ for path in (
+ *_python_files(PRODUCT),
+ *_python_files(PYQT_REACTIVE),
+ *_python_files(FORMBRIDGE),
+ )
if "OPENHCS_USE_EXTERNAL_EDITOR" in path.read_text(encoding="utf-8")
)
assert readers == ["openhcs/pyqt_gui/services/reactor_providers.py"]
diff --git a/tests/unit/pyqt_gui/test_interval_snapshot_poller.py b/tests/unit/pyqt_gui/test_interval_snapshot_poller.py
index d3f2790bb3..024b05f1a6 100644
--- a/tests/unit/pyqt_gui/test_interval_snapshot_poller.py
+++ b/tests/unit/pyqt_gui/test_interval_snapshot_poller.py
@@ -1,7 +1,7 @@
import threading
import time
-from pyqt_reactive.services.interval_snapshot_poller import (
+from formbridge.services.interval_snapshot_poller import (
CallbackIntervalSnapshotPollerPolicy,
IntervalSnapshotPoller,
)
diff --git a/tests/unit/pyqt_gui/test_live_server_tree_sync.py b/tests/unit/pyqt_gui/test_live_server_tree_sync.py
index d5c22fa038..5c7b545206 100644
--- a/tests/unit/pyqt_gui/test_live_server_tree_sync.py
+++ b/tests/unit/pyqt_gui/test_live_server_tree_sync.py
@@ -10,11 +10,8 @@
except ImportError:
PYQT_AVAILABLE = False
-from pyqt_reactive.services.zmq_server_info import (
- BaseServerInfo,
- ExecutionServerInfo,
-)
-from pyqt_reactive.services.zmq_server_scan_service import StartingEndpointObservation
+from formbridge.services.zmq_server_info import BaseServerInfo, ExecutionServerInfo
+from formbridge.services.zmq_server_scan_service import StartingEndpointObservation
from zmqruntime.messages import PongResponse
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
diff --git a/tests/unit/pyqt_gui/test_logging_config.py b/tests/unit/pyqt_gui/test_logging_config.py
index b7425ae620..a4296c86f9 100644
--- a/tests/unit/pyqt_gui/test_logging_config.py
+++ b/tests/unit/pyqt_gui/test_logging_config.py
@@ -6,13 +6,12 @@
from pathlib import Path
from unittest.mock import patch
-from pyqt_reactive.protocols import (
+from formbridge.protocols.component_selection import (
ComponentSelectionProviderABC,
FunctionSelectionProviderABC,
- LogDiscoveryProviderABC,
- ServerScanProviderABC,
)
-from pyqt_reactive.services.zmq_server_scan_service import (
+from formbridge.protocols.log_providers import LogDiscoveryProviderABC, ServerScanProviderABC
+from formbridge.services.zmq_server_scan_service import (
EndpointObservationSnapshot,
ZMQServerScanService,
)
diff --git a/tests/unit/pyqt_gui/test_main_config_propagation.py b/tests/unit/pyqt_gui/test_main_config_propagation.py
index db75ee27fb..514d8a05f0 100644
--- a/tests/unit/pyqt_gui/test_main_config_propagation.py
+++ b/tests/unit/pyqt_gui/test_main_config_propagation.py
@@ -5,9 +5,7 @@
import pytest
from PyQt6.QtWidgets import QWidget
-from pyqt_reactive.services.async_operation_executor import (
- AsyncOperationExecutorClosedError,
-)
+from formbridge.services.async_operation_executor import AsyncOperationExecutorClosedError
import openhcs.pyqt_gui.main as main_module
from openhcs.core.config import GlobalPipelineConfig
@@ -337,9 +335,7 @@ def record(self, value=None, *additional_values, **named_values) -> None:
def test_list_preview_config_applies_to_both_real_lists(qtbot) -> None:
from pyqt_reactive.core import ReorderableListWidget
- from pyqt_reactive.strategies.preview_formatting import (
- ObjectStatePreviewFormattingService,
- )
+ from formbridge.strategies.preview_formatting import ObjectStatePreviewFormattingService
from pyqt_reactive.widgets.shared.abstract_manager_widget import (
AbstractManagerWidget,
)
diff --git a/tests/unit/pyqt_gui/test_main_status_bar.py b/tests/unit/pyqt_gui/test_main_status_bar.py
index f65e455c30..62a60718bc 100644
--- a/tests/unit/pyqt_gui/test_main_status_bar.py
+++ b/tests/unit/pyqt_gui/test_main_status_bar.py
@@ -7,9 +7,7 @@
from objectstate.object_state import ObjectStateRegistry
from PyQt6.QtCore import QCoreApplication, QEvent, pyqtSignal
from PyQt6.QtWidgets import QApplication, QMainWindow
-from pyqt_reactive.services.zmq_server_scan_service import (
- EndpointObservationSnapshot,
-)
+from formbridge.services.zmq_server_scan_service import EndpointObservationSnapshot
from pyqt_reactive.theming import ColorScheme
from zmqruntime.messages import PongResponse, ServerRole
diff --git a/tests/unit/pyqt_gui/test_main_window_file_actions.py b/tests/unit/pyqt_gui/test_main_window_file_actions.py
index 5cecbf11dd..1c69f81846 100644
--- a/tests/unit/pyqt_gui/test_main_window_file_actions.py
+++ b/tests/unit/pyqt_gui/test_main_window_file_actions.py
@@ -6,7 +6,7 @@
import pytest
-from openhcs.core.selection import AllItems, SelectedItems
+from formbridge.selection import AllItems, SelectedItems
from openhcs.pyqt_gui.main import OpenHCSMainWindow
diff --git a/tests/unit/pyqt_gui/test_main_zmq_window_routing.py b/tests/unit/pyqt_gui/test_main_zmq_window_routing.py
index 8c757e11c4..ab4a9cbc8b 100644
--- a/tests/unit/pyqt_gui/test_main_zmq_window_routing.py
+++ b/tests/unit/pyqt_gui/test_main_zmq_window_routing.py
@@ -9,10 +9,8 @@
from PyQt6.QtGui import QCloseEvent
from PyQt6.QtWidgets import QDialog, QTreeWidgetItem
from pyqt_reactive.services.window_manager import WindowManager
-from pyqt_reactive.services.zmq_server_info import BaseServerInfo
-from pyqt_reactive.services.zmq_server_scan_service import (
- EndpointObservationSnapshot,
-)
+from formbridge.services.zmq_server_info import BaseServerInfo
+from formbridge.services.zmq_server_scan_service import EndpointObservationSnapshot
from pyqt_reactive.widgets import StatusState
from pyqt_reactive.widgets.shared.zmq_server_browser_widget import (
ZMQServerBrowserWidgetABC,
diff --git a/tests/unit/pyqt_gui/test_object_state_callable_preview.py b/tests/unit/pyqt_gui/test_object_state_callable_preview.py
index b9a9fa47ba..a42155c4ca 100644
--- a/tests/unit/pyqt_gui/test_object_state_callable_preview.py
+++ b/tests/unit/pyqt_gui/test_object_state_callable_preview.py
@@ -2,11 +2,9 @@
import pytest
-from openhcs.agent.dto.ui_bridge import UiObjectStateScopeListRequest
+from formbridge.bridge.dto import UiObjectStateScopeListRequest
from objectstate.object_state import ObjectState, ObjectStateRegistry
-from openhcs.pyqt_gui.services.ui_bridge_object_state import (
- ObjectStateScopeProjectionService,
-)
+from formbridge.bridge.object_state import ObjectStateScopeProjectionService
def preview_test_callable(image, sigma: float = 1.0):
diff --git a/tests/unit/pyqt_gui/test_one_persistence_format.py b/tests/unit/pyqt_gui/test_one_persistence_format.py
index 9eb2b3217d..b344c53136 100644
--- a/tests/unit/pyqt_gui/test_one_persistence_format.py
+++ b/tests/unit/pyqt_gui/test_one_persistence_format.py
@@ -5,12 +5,10 @@
import ast
from pathlib import Path
-from pyqt_reactive.protocols import register_codegen_provider
-from pyqt_reactive.services.function_pattern_code_document import (
- FunctionPatternCodeDocumentService,
-)
+from formbridge.protocols.codegen_provider import register_codegen_provider
+from formbridge.services.function_pattern_code_document import FunctionPatternCodeDocumentService
-from pyqt_reactive.core.path_cache import PathCacheKey
+from formbridge.core.path_cache import PathCacheKey
import openhcs.pyqt_gui.widgets.plate_manager # noqa: F401 - declares its dialog context
from openhcs.core.steps.function_step import FunctionStep
diff --git a/tests/unit/pyqt_gui/test_pipeline_editor_widget.py b/tests/unit/pyqt_gui/test_pipeline_editor_widget.py
index eee3ef12f2..bc9e1b9af5 100644
--- a/tests/unit/pyqt_gui/test_pipeline_editor_widget.py
+++ b/tests/unit/pyqt_gui/test_pipeline_editor_widget.py
@@ -13,16 +13,10 @@
FormManagerConfig,
ParameterFormManager,
)
-from pyqt_reactive.services.function_navigation import (
- build_function_token_field_path,
-)
-from pyqt_reactive.services.function_pattern_code_document import (
- FunctionPatternCodeDocumentService,
-)
-from pyqt_reactive.services.pattern_data_manager import (
- FUNC_EDITOR_PATTERN_TOKENS_META_KEY,
-)
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.function_navigation import build_function_token_field_path
+from formbridge.services.function_pattern_code_document import FunctionPatternCodeDocumentService
+from formbridge.services.pattern_data_manager import FUNC_EDITOR_PATTERN_TOKENS_META_KEY
+from formbridge.services.scope_token_service import ScopeTokenService
from pyqt_reactive.theming import ColorScheme
from openhcs.authoring.session.compilation import CompiledDataset
@@ -59,7 +53,7 @@
stack_percentile_normalize,
)
from openhcs.pyqt_gui.windows.dual_editor_window import DualEditorWindow
-from pyqt_reactive.services.code_editor_form_updater import CodeEditorFormUpdater
+from formbridge.services.code_editor_form_updater import CodeEditorFormUpdater
from openhcs.ui.shared.plate_scope_identity import PipelineScopeIdentity
from tests.unit.pyqt_gui.session_harness import add_datasets, qt_app, session_gui
from openhcs.core.debug import StepDebugCommand
diff --git a/tests/unit/pyqt_gui/test_pipeline_history_occurrence_restart_regression.py b/tests/unit/pyqt_gui/test_pipeline_history_occurrence_restart_regression.py
index 4b7e50aa04..68ef1ec3f3 100644
--- a/tests/unit/pyqt_gui/test_pipeline_history_occurrence_restart_regression.py
+++ b/tests/unit/pyqt_gui/test_pipeline_history_occurrence_restart_regression.py
@@ -12,14 +12,12 @@
import pytest
from objectstate import ObjectStateRegistry
-from pyqt_reactive.services.function_pattern_code_document import (
+from formbridge.services.function_pattern_code_document import (
FunctionPatternCodeDocumentService,
function_pattern_authority,
)
-from pyqt_reactive.services.pattern_data_manager import (
- FUNC_EDITOR_PATTERN_TOKENS_META_KEY,
-)
-from pyqt_reactive.services.scope_token_service import ScopeTokenService
+from formbridge.services.pattern_data_manager import FUNC_EDITOR_PATTERN_TOKENS_META_KEY
+from formbridge.services.scope_token_service import ScopeTokenService
from openhcs.core.config import PipelineConfig
from openhcs.core.pipeline_document import PipelineDocumentCodec
diff --git a/tests/unit/pyqt_gui/test_plate_manager_widget.py b/tests/unit/pyqt_gui/test_plate_manager_widget.py
index e890e9b31c..4c38f7cb55 100644
--- a/tests/unit/pyqt_gui/test_plate_manager_widget.py
+++ b/tests/unit/pyqt_gui/test_plate_manager_widget.py
@@ -16,13 +16,11 @@
from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus
import openhcs.processing.backends.cellprofiler as cellprofiler_backend
-from openhcs.core.selection import AllItems, SelectedItems
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.selection import AllItems, SelectedItems
+from formbridge.bridge.dto import (
UiBridgeConfirmationRequirement,
UiCodeDocumentApplyRequest,
- UiCodeDocumentId,
UiCodeDocumentRequest,
- UiStateSurfaceId,
UiStateSurfaceRequest,
UiWindowNavigateRequest,
)
@@ -97,14 +95,12 @@
MainWindowEmbeddedWidgets,
MainWindowPipelineActions,
)
-from openhcs.pyqt_gui.services.ui_agent_bridge import UiAgentBridgeService
-from openhcs.pyqt_gui.services.ui_bridge_object_state import (
- ObjectStateBridgeProviderSet,
-)
+from formbridge.bridge.in_process import UiAgentBridgeService
+from pyqt_reactive.bridge.object_state_documents import ObjectStateBridgeProviderSet
from openhcs.pyqt_gui.services.ui_bridge_plate_manager import (
PlateManagerBridgeProviderSet,
)
-from openhcs.pyqt_gui.services.ui_bridge_windows import UiWindowProjectionService
+from pyqt_reactive.bridge.windows import UiWindowProjectionService
from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
from openhcs.ui.shared.plate_manager_code_document import (
PlateManagerCodeDocumentAuthority,
@@ -118,6 +114,11 @@
RunningProgressStatus,
StepStartedProgressPhase,
)
+from openhcs.agent.ui_bridge_identities import (
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+)
+from openhcs.pyqt_gui.services.ui_agent_bridge import OpenHCSUiAgentBridgeService
class InlineUiThreadDispatcher:
@@ -165,7 +166,7 @@ def _workspace(steps, pipeline_config: PipelineConfig | None = None):
def _bridge(manager) -> UiAgentBridgeService:
- return UiAgentBridgeService(
+ return OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(manager),
dispatcher=InlineUiThreadDispatcher(),
)
@@ -174,7 +175,7 @@ def _bridge(manager) -> UiAgentBridgeService:
def _apply(bridge, source: str, *, selection_mode=AllItems):
document = bridge.get_document(
UiCodeDocumentRequest(
- document_id=UiCodeDocumentId.PLATE_MANAGER_ORCHESTRATOR.value,
+ document_id=DatasetListCodeDocumentIdentity.require_value(),
selection_mode=selection_mode.name,
)
)
@@ -1266,7 +1267,7 @@ def test_dataset_document_edits_during_a_run_and_recovers_after_its_failure(
result = _apply(bridge, replacement)
state = bridge.get_state_surface(
UiStateSurfaceRequest(
- surface_id=UiStateSurfaceId.PLATE_MANAGER.value,
+ surface_id=DatasetListStateSurfaceIdentity.require_value(),
selection_mode=AllItems.name,
)
)
@@ -1296,7 +1297,7 @@ def test_a_failure_is_presented_only_after_the_run_finalizes(tmp_path) -> None:
WindowManager.register(
window_id, editor, code_document_driver=editor.code_document_driver()
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=ObjectStateBridgeProviderSet(),
dispatcher=InlineUiThreadDispatcher(),
)
diff --git a/tests/unit/pyqt_gui/test_progress_tree_aggregation.py b/tests/unit/pyqt_gui/test_progress_tree_aggregation.py
index cf3af33b63..aeff6bad54 100644
--- a/tests/unit/pyqt_gui/test_progress_tree_aggregation.py
+++ b/tests/unit/pyqt_gui/test_progress_tree_aggregation.py
@@ -1,6 +1,6 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTreeWidget, QTreeWidgetItem
-from pyqt_reactive.services.zmq_server_info import BaseServerInfo
+from formbridge.services.zmq_server_info import BaseServerInfo
from pyqt_reactive.widgets.shared import TreeStateAdapter, TreeSyncAdapter
from zmqruntime.messages import PongResponse
diff --git a/tests/unit/pyqt_gui/test_server_row_presenter.py b/tests/unit/pyqt_gui/test_server_row_presenter.py
index b6905a600a..81917801c9 100644
--- a/tests/unit/pyqt_gui/test_server_row_presenter.py
+++ b/tests/unit/pyqt_gui/test_server_row_presenter.py
@@ -7,7 +7,7 @@
except Exception:
PYQT_AVAILABLE = False
-from pyqt_reactive.services.zmq_server_info import BaseServerInfo
+from formbridge.services.zmq_server_info import BaseServerInfo
from zmqruntime.messages import PongResponse
from openhcs.pyqt_gui.widgets.shared.server_browser.presentation_models import (
diff --git a/tests/unit/pyqt_gui/test_source_binding_preview_policy.py b/tests/unit/pyqt_gui/test_source_binding_preview_policy.py
index 75ab576226..5aa00988e8 100644
--- a/tests/unit/pyqt_gui/test_source_binding_preview_policy.py
+++ b/tests/unit/pyqt_gui/test_source_binding_preview_policy.py
@@ -5,13 +5,8 @@
from openhcs.core.config import SourceBindingsConfig
from openhcs.core.source_bindings import NamedSourceBinding
from openhcs.pyqt_gui.config import UIConfig
-from pyqt_reactive.utils.preview_formatters import (
- PreviewFieldFormatRequest,
- PreviewValueDetail,
-)
-from pyqt_reactive.widgets.shared.manager_preview_formatting import (
- ManagerPreviewFieldFormatter,
-)
+from formbridge.utils.preview_formatters import PreviewFieldFormatRequest, PreviewValueDetail
+from formbridge.managers.preview_formatting import ManagerPreviewFieldFormatter
def test_source_binding_collection_is_compact_and_expansion_preserves_its_scope():
diff --git a/tests/unit/pyqt_gui/test_source_bindings_editor.py b/tests/unit/pyqt_gui/test_source_bindings_editor.py
index abc260a3e4..0ec36f25fc 100644
--- a/tests/unit/pyqt_gui/test_source_bindings_editor.py
+++ b/tests/unit/pyqt_gui/test_source_bindings_editor.py
@@ -73,10 +73,7 @@
ParameterFormManager,
)
from pyqt_reactive.forms.widget_strategies import PlaceholderConfig, PyQt6WidgetEnhancer
-from pyqt_reactive.forms.parameter_info_types import (
- InlineDataclassWidgetInfo,
- create_parameter_info,
-)
+from formbridge.forms.parameter_info_types import InlineDataclassWidgetInfo, create_parameter_info
from pyqt_reactive.widgets.shared.scrollable_form_mixin import ScrollableFormMixin
from pyqt_reactive.widgets.shared.scrollable_form_mixin import ScrollViewport
from pyqt_reactive.widgets.shared.reflowing_vertical_scroll_area import (
diff --git a/tests/unit/pyqt_gui/test_step_code_widget_roundtrip.py b/tests/unit/pyqt_gui/test_step_code_widget_roundtrip.py
index 4c77346bdf..e79f174170 100644
--- a/tests/unit/pyqt_gui/test_step_code_widget_roundtrip.py
+++ b/tests/unit/pyqt_gui/test_step_code_widget_roundtrip.py
@@ -12,11 +12,8 @@
OpenHCSComponentSelectionProvider,
)
from openhcs.pyqt_gui.widgets.step_parameter_editor import StepParameterEditorWidget
-from pyqt_reactive.protocols import (
- FunctionSelectionProviderABC,
- codegen_provider,
- component_selection,
-)
+from formbridge.protocols.component_selection import FunctionSelectionProviderABC
+from formbridge.protocols import codegen_provider, component_selection
from pyqt_reactive.widgets.function_list_editor import FunctionListEditorWidget
diff --git a/tests/unit/pyqt_gui/test_system_monitor_widget.py b/tests/unit/pyqt_gui/test_system_monitor_widget.py
index aceee3c79b..b929db37b1 100644
--- a/tests/unit/pyqt_gui/test_system_monitor_widget.py
+++ b/tests/unit/pyqt_gui/test_system_monitor_widget.py
@@ -9,16 +9,16 @@
from PyQt6.QtWidgets import QSizePolicy
import pyqt_reactive.widgets.system_monitor as system_monitor
-from pyqt_reactive.services.system_metrics_sampler import (
+from formbridge.services.system_metrics_sampler import (
CpuMetrics,
MemoryMetrics,
SystemMetricsSamplerConfig,
)
-from pyqt_reactive.services.system_monitor_config import (
+from formbridge.services.system_monitor_config import (
PerformanceMonitorColors,
PerformanceMonitorConfig,
)
-from pyqt_reactive.services.system_metrics_sampler import SystemMetrics
+from formbridge.services.system_metrics_sampler import SystemMetrics
from pyqt_reactive.widgets.system_monitor import (
SystemMonitorAction,
SystemMonitorGraphLayout,
diff --git a/tests/unit/pyqt_gui/test_tree_state_adapter.py b/tests/unit/pyqt_gui/test_tree_state_adapter.py
index 6ce25b1d32..21e8c2968a 100644
--- a/tests/unit/pyqt_gui/test_tree_state_adapter.py
+++ b/tests/unit/pyqt_gui/test_tree_state_adapter.py
@@ -8,7 +8,7 @@
except Exception:
PYQT_AVAILABLE = False
-from pyqt_reactive.services.zmq_server_info import BaseServerInfo
+from formbridge.services.zmq_server_info import BaseServerInfo
from pyqt_reactive.widgets.shared import TreeNodeIdentity, TreeStateAdapter
from zmqruntime.messages import PongResponse, ServerRole
diff --git a/tests/unit/pyqt_gui/test_ui_agent_bridge.py b/tests/unit/pyqt_gui/test_ui_agent_bridge.py
index 9e4df38287..a930690b19 100644
--- a/tests/unit/pyqt_gui/test_ui_agent_bridge.py
+++ b/tests/unit/pyqt_gui/test_ui_agent_bridge.py
@@ -23,10 +23,8 @@
QVBoxLayout,
QWidget,
)
-from pyqt_reactive.protocols import register_codegen_provider
-from pyqt_reactive.services.pattern_data_manager import (
- FUNC_EDITOR_PATTERN_TOKENS_META_KEY,
-)
+from formbridge.protocols.codegen_provider import register_codegen_provider
+from formbridge.services.pattern_data_manager import FUNC_EDITOR_PATTERN_TOKENS_META_KEY
from pyqt_reactive.services.ui_thread_dispatch import UiThreadDispatcher
from pyqt_reactive.services.widget_tree_projection import (
WidgetActionKind,
@@ -34,12 +32,9 @@
WidgetRect,
WidgetTreeProjection,
)
-from pyqt_reactive.services.window_code_document import (
- WindowCodeDocument,
- WindowCodeDocumentDriver,
-)
+from formbridge.services.window_code_document import WindowCodeDocument, WindowCodeDocumentDriver
from pyqt_reactive.services.window_manager import WindowManager
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from pyqt_reactive.widgets.editors import simple_code_editor
from pyqt_reactive.widgets.editors.simple_code_editor import SimpleCodeEditorService
from pyqt_reactive.widgets.shared import (
@@ -56,9 +51,9 @@
from zmqruntime import DataControlPortPairAuthority, EndpointApplication
from zmqruntime.config import TransportMode
-from openhcs.core.selection import AllItems, SelectedItems
-from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.selection import AllItems, SelectedItems
+from formbridge.contracts import SCHEMA_VERSION
+from formbridge.bridge.dto import (
ActionAccepted,
ActionRejected,
DirtyFields,
@@ -69,15 +64,11 @@
UiBridgeOperationStatusRequest,
UiBridgeRequestEnvelope,
UiCodeDocumentApplyRequest,
- UiCodeDocumentId,
UiCodeDocumentRequest,
UiCodeDocumentValidationRequest,
UiObjectStateScopeListRequest,
- UiSelectedPlateWorkflowKind,
- UiSelectedPlateWorkflowRequest,
UiSnapshotListRequest,
UiSnapshotRestoreRequest,
- UiStateSurfaceId,
UiStateSurfaceRequest,
UiWidgetActionInvokeRequest,
UiWidgetActionIssueCode,
@@ -91,13 +82,13 @@
UiWindowSnapshotRequest,
UiWindowSummary,
)
-from openhcs.agent.services.ui_bridge_service import (
+from openhcs.agent.dto.ui_bridge import UiSelectedPlateWorkflowKind, UiSelectedPlateWorkflowRequest
+from formbridge.bridge.service import (
UiBridgeGetDocumentOperation,
UiBridgeGetStateSurfaceOperation,
UiBridgeListDocumentsOperation,
UiBridgeListStateSurfacesOperation,
UiBridgeStatusOperation,
- UI_BRIDGE_PROTOCOL_VERSION,
UiBridgeConnectionResolution,
UiBridgeService,
)
@@ -161,27 +152,25 @@
MainWindowEmbeddedWidgets,
)
from openhcs.pyqt_gui.services.reactor_providers import OpenHCSCodegenProvider
-from openhcs.pyqt_gui.services.ui_agent_bridge import (
+from formbridge.bridge.in_process import (
UiAgentBridgeService,
UiBridgeOperationTracker,
- UiCodeDocumentSourcePolicy,
UiObjectStateSnapshotProvider,
)
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- state_surface_ids_for_action,
-)
+from openhcs.pyqt_gui.services.ui_agent_bridge import UiCodeDocumentSourcePolicy
+from formbridge.bridge.contracts import state_surface_ids_for_action
from openhcs.pyqt_gui.services.ui_bridge_composition import (
OpenHCSUiBridgeCompositionRoot,
)
-from openhcs.pyqt_gui.services.ui_bridge_live_overview import (
- LiveOverviewBridgeProviderSet,
-)
-from openhcs.pyqt_gui.services.ui_bridge_object_state import (
+from openhcs.pyqt_gui.services.ui_bridge_live_overview import LiveOverviewBridgeProviderSet
+from formbridge.bridge.object_state import (
OBJECT_STATE_SCOPE_CODE_DOCUMENT_PREFIX,
WINDOW_CODE_DOCUMENT_PREFIX,
+ ObjectStateScopeProjectionService,
+)
+from pyqt_reactive.bridge.object_state_documents import (
ObjectStateBridgeProviderSet,
ObjectStateScopeCodeDocumentProvider,
- ObjectStateScopeProjectionService,
)
from openhcs.pyqt_gui.services.ui_bridge_pipeline_editor import (
PipelineEditorBridgeProviderSet,
@@ -189,18 +178,18 @@
from openhcs.pyqt_gui.services.ui_bridge_plate_manager import (
PlateManagerBridgeProviderSet,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import (
+from formbridge.bridge.registry import (
UiBridgeProviderSetABC,
UiBridgeRegistrationContext,
UiBridgeSurfaceRegistry,
)
-from openhcs.pyqt_gui.services.ui_bridge_server import (
+from formbridge.bridge.server import (
UiBridgeControlServer,
UiBridgeRequestDispatcher,
UiBridgeServerBinding,
)
-from openhcs.pyqt_gui.services.ui_bridge_windows import (
- MainWindowBridgeProviderSet,
+from openhcs.pyqt_gui.services.ui_bridge_windows import MainWindowBridgeProviderSet
+from pyqt_reactive.bridge.windows import (
QtTopLevelWindowProjection,
UiWidgetTreeResultFactory,
UiWindowProjectionService,
@@ -234,8 +223,19 @@
StepDebugCommand,
StopDebugCommand,
)
+from openhcs.agent.ui_bridge_identities import (
+ PipelineDebugSessionStateSurfaceIdentityDeclaration,
+ PipelineEditorStateSurfaceIdentityDeclaration,
+ DatasetListCodeDocumentIdentity,
+ DatasetListStateSurfaceIdentity,
+)
+from formbridge.bridge.identities import UiLiveOverviewStateSurfaceIdentityDeclaration
+from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
+from openhcs.pyqt_gui.services.ui_agent_bridge import OpenHCSUiAgentBridgeService
+from pyqt_reactive.bridge.host import UiBridgeHostWindow
+from openhcs.agent.ui_bridge_identities import MainWindowWidgetIdentity
-DOCUMENT_ID = UiCodeDocumentId.PLATE_MANAGER_ORCHESTRATOR.value
+DOCUMENT_ID = DatasetListCodeDocumentIdentity.require_value()
PLATE_SCOPE_ID = "plate-1"
DATASET_SCOPE_ID = "/tmp/openhcs-ui-bridge-test/plate-1"
PLATE_NAME = "plate 1"
@@ -676,7 +676,7 @@ def test_atomic_success_does_not_record_snapshot_on_failure() -> None:
def test_selected_read_fails_loudly_when_no_plate_is_selected() -> None:
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager())
)
@@ -699,7 +699,7 @@ def dataset_document(self, selection_mode):
del selection_mode
raise RuntimeError("context construction failed")
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FailingPlateManager(selected=()))
)
@@ -898,7 +898,7 @@ def test_listed_embedded_window_routes_support_widget_tree_projection(
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -920,7 +920,7 @@ def test_listed_embedded_window_routes_support_widget_tree_projection(
widget_tree = bridge.widget_tree(
UiWidgetTreeRequest(
- window_id="plate_manager",
+ window_id="datasets",
open_policy=UiWindowOpenPolicy(),
include_tree=True,
)
@@ -964,7 +964,7 @@ def test_embedded_window_focus_uses_authoritative_dock_pane() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -991,9 +991,13 @@ def test_embedded_window_focus_uses_authoritative_dock_pane() -> None:
system_pane.dock_widget.close()
+class _BridgeHostWindow(QMainWindow, UiBridgeHostWindow):
+ UI_BRIDGE_WIDGET_IDENTITY = MainWindowWidgetIdentity
+
+
def test_floating_embedded_pane_is_not_duplicated_as_qt_top_level() -> None:
app = QtApplicationAuthority.app()
- main_window = QMainWindow()
+ main_window = _BridgeHostWindow()
main_window.session = BridgeSessions.get()
embedded_widgets = MainWindowEmbeddedWidgets()
pipeline_pane = DockPane.create(
@@ -1024,7 +1028,7 @@ def test_floating_embedded_pane_is_not_duplicated_as_qt_top_level() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1051,7 +1055,7 @@ def test_embedded_registration_does_not_hide_main_window_from_bridge(
tmp_path: Path,
) -> None:
app = QtApplicationAuthority.app()
- main_window = QMainWindow()
+ main_window = _BridgeHostWindow()
main_window.session = BridgeSessions.get()
main_window.setWindowTitle("OpenHCS")
embedded_widgets = MainWindowEmbeddedWidgets()
@@ -1085,7 +1089,7 @@ def test_embedded_registration_does_not_hide_main_window_from_bridge(
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1172,7 +1176,7 @@ def test_widget_tree_item_rows_carry_shared_object_state_roles() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1580,7 +1584,7 @@ def test_listed_qt_top_level_window_supports_operation_request_resolution() -> N
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1638,7 +1642,7 @@ def test_projected_widget_action_invokes_live_button_by_path_id() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1709,7 +1713,7 @@ def test_projected_widget_action_selects_live_item_view_row_by_path_id() -> None
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1791,7 +1795,7 @@ def test_projected_widget_action_selects_standalone_tab_bar_by_index() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1870,7 +1874,7 @@ def test_projected_widget_action_selects_combo_box_by_index() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -1943,7 +1947,7 @@ def test_legacy_empty_scope_window_catalogs_as_global_config(tmp_path: Path) ->
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2035,7 +2039,7 @@ def test_managed_window_catalog_projects_object_state_status() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2090,7 +2094,7 @@ def test_open_window_code_mode_documents_are_discoverable_by_window_id() -> None
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2143,7 +2147,7 @@ def apply_source(self, source: str) -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2211,7 +2215,7 @@ def apply_source(self, source: str) -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2270,7 +2274,7 @@ def records_object_state_snapshot_on_apply(self) -> bool:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2336,7 +2340,7 @@ def test_open_window_code_mode_summary_uses_driver_title_when_window_title_empty
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2387,7 +2391,7 @@ def test_simple_code_editor_windows_register_code_documents(monkeypatch) -> None
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2461,7 +2465,7 @@ def test_object_state_code_mode_documents_are_discoverable_by_scope_id() -> None
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -2668,7 +2672,7 @@ def test_managed_window_save_action_returns_before_deferred_save_runs() -> None:
snapshot_provider=snapshot_provider,
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=dispatcher,
snapshot_provider=snapshot_provider,
@@ -2721,7 +2725,7 @@ def test_managed_window_save_action_returns_before_deferred_save_runs() -> None:
def test_all_read_returns_source_hash_and_revision() -> None:
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager())
)
@@ -2740,7 +2744,7 @@ def test_all_read_returns_source_hash_and_revision() -> None:
def test_validation_rejects_side_effecting_source_before_execution() -> None:
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager())
)
@@ -2757,7 +2761,7 @@ def test_validation_rejects_side_effecting_source_before_execution() -> None:
def test_validation_rejects_legacy_pipeline_config_assignment() -> None:
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager())
)
@@ -2778,7 +2782,7 @@ def test_validation_rejects_legacy_pipeline_config_assignment() -> None:
def test_validation_reports_orchestrator_payload_hint() -> None:
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager())
)
@@ -2807,7 +2811,7 @@ def test_apply_rejection_reports_current_revision_and_snapshot() -> None:
state = ObjectState(Dummy(), scope_id=PLATE_SCOPE_ID)
ObjectStateRegistry.register(state, _skip_snapshot=True)
ObjectStateRegistry.record_snapshot("before edit", scope_id=PLATE_SCOPE_ID)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(
FakePlateManager(
selected=(PLATE_SCOPE_ID,),
@@ -2842,7 +2846,7 @@ def test_confirmation_required_apply_rejection_reports_current_context() -> None
state = ObjectState(Dummy(), scope_id=PLATE_SCOPE_ID)
ObjectStateRegistry.register(state, _skip_snapshot=True)
ObjectStateRegistry.record_snapshot("before edit", scope_id=PLATE_SCOPE_ID)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(
FakePlateManager(
selected=(PLATE_SCOPE_ID,),
@@ -3038,7 +3042,7 @@ def test_apply_creates_baseline_and_edit_snapshot() -> None:
state = ObjectState(Dummy(), scope_id=PLATE_SCOPE_ID)
ObjectStateRegistry.register(state, _skip_snapshot=True)
operations = FakeOperations(state)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(
FakePlateManager(operations=operations)
),
@@ -3091,7 +3095,7 @@ def test_apply_document_returns_running_operation_before_queued_ui_apply_runs()
operations = FakeOperations(state)
dispatcher = QueuedPostDispatcher()
operation_tracker = UiBridgeOperationTracker()
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(
FakePlateManager(operations=operations)
),
@@ -3136,7 +3140,7 @@ def test_queued_apply_document_error_updates_operation_status() -> None:
operations = FakeOperations(state)
dispatcher = QueuedPostDispatcher()
operation_tracker = UiBridgeOperationTracker()
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(
FakePlateManager(operations=operations)
),
@@ -3187,7 +3191,7 @@ def test_live_overview_reports_running_and_completed_bridge_operations() -> None
context
)
LiveOverviewBridgeProviderSet().register(context)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=dispatcher,
snapshot_provider=snapshot_provider,
@@ -3209,7 +3213,7 @@ def test_live_overview_reports_running_and_completed_bridge_operations() -> None
)
)
running_overview = bridge.get_state_surface(
- UiStateSurfaceRequest(surface_id=UiStateSurfaceId.UI_LIVE_OVERVIEW.value)
+ UiStateSurfaceRequest(surface_id=UiLiveOverviewStateSurfaceIdentityDeclaration.require_value())
)
running_section = next(
@@ -3224,7 +3228,7 @@ def test_live_overview_reports_running_and_completed_bridge_operations() -> None
dispatcher.run_next()
completed_overview = bridge.get_state_surface(
- UiStateSurfaceRequest(surface_id=UiStateSurfaceId.UI_LIVE_OVERVIEW.value)
+ UiStateSurfaceRequest(surface_id=UiLiveOverviewStateSurfaceIdentityDeclaration.require_value())
)
completed_section = next(
section
@@ -3246,7 +3250,7 @@ def test_bridge_lists_and_restores_snapshots() -> None:
before_id = ObjectStateRegistry.get_branch_history()[-1].id
state.update_parameter("x", 2)
ObjectStateRegistry.record_snapshot("after", scope_id=PLATE_SCOPE_ID)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager()),
dispatcher=InlineDispatcher(),
)
@@ -3286,7 +3290,7 @@ def test_bridge_switches_to_preserved_future_branch() -> None:
for branch in ObjectStateRegistry.list_branches()
if branch["name"].startswith("auto-")
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager()),
dispatcher=InlineDispatcher(),
)
@@ -3321,7 +3325,7 @@ def test_snapshot_restore_authorization_rejects_before_object_state_change() ->
)
)
)
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(FakePlateManager()),
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -3346,7 +3350,7 @@ def test_ui_bridge_control_server_round_trips_documents_through_descriptor(
monkeypatch.setenv("OPENHCS_UI_BRIDGE_DESCRIPTOR_DIR", str(tmp_path))
manager = FakePlateManager(selected=(PLATE_SCOPE_ID,))
manager.session.ensure_datasets((DATASET_SCOPE_ID,))
- bridge = UiAgentBridgeService(
+ bridge = OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(manager),
dispatcher=InlineDispatcher(),
)
@@ -3378,7 +3382,7 @@ def test_ui_bridge_control_server_round_trips_documents_through_descriptor(
state = service.invoke(
UiBridgeGetStateSurfaceOperation,
UiStateSurfaceRequest(
- surface_id=UiStateSurfaceId.PLATE_MANAGER.value,
+ surface_id=DatasetListStateSurfaceIdentity.require_value(),
selection_mode=ALL_SELECTION_MODE,
),
connection,
@@ -3391,7 +3395,7 @@ def test_ui_bridge_control_server_round_trips_documents_through_descriptor(
assert BRIDGE_AUTH_TOKEN not in set(_json_payload_values(to_jsonable(status)))
assert catalog.documents[0].document_id == DOCUMENT_ID
assert (
- state_catalog.surfaces[0].surface_id == UiStateSurfaceId.PLATE_MANAGER.value
+ state_catalog.surfaces[0].surface_id == DatasetListStateSurfaceIdentity.require_value()
)
assert document.source == VALID_SOURCE
assert state.payload["rows"][0]["scope_id"] == DATASET_SCOPE_ID
@@ -3416,7 +3420,7 @@ def test_two_ipc_ui_bridges_route_each_descriptor_to_its_exact_instance(
)
descriptor_directory = tmp_path / "descriptors"
first_server = UiBridgeControlServer(
- UiAgentBridgeService(dispatcher=InlineDispatcher()),
+ OpenHCSUiAgentBridgeService(dispatcher=InlineDispatcher()),
AgentUiBridgeConfig(
port=first_pair.data_port,
transport_mode=TransportMode.IPC,
@@ -3426,7 +3430,7 @@ def test_two_ipc_ui_bridges_route_each_descriptor_to_its_exact_instance(
),
)
second_server = UiBridgeControlServer(
- UiAgentBridgeService(dispatcher=InlineDispatcher()),
+ OpenHCSUiAgentBridgeService(dispatcher=InlineDispatcher()),
AgentUiBridgeConfig(
port=second_pair.data_port,
transport_mode=TransportMode.IPC,
@@ -3467,7 +3471,7 @@ def test_two_ipc_ui_bridges_route_each_descriptor_to_its_exact_instance(
def test_ui_bridge_control_server_preserves_bad_auth_error(tmp_path: Path) -> None:
- bridge = UiAgentBridgeService(dispatcher=InlineDispatcher())
+ bridge = OpenHCSUiAgentBridgeService(dispatcher=InlineDispatcher())
server = UiBridgeControlServer(
bridge,
_bridge_server_config(tmp_path),
@@ -3502,7 +3506,7 @@ def test_ui_bridge_control_server_preserves_bad_auth_error(tmp_path: Path) -> No
"Unsupported UI bridge protocol version",
),
(
- UI_BRIDGE_PROTOCOL_VERSION,
+ OpenHCSUiBridgeApplication.protocol_version,
EndpointApplication(identifier="openhcs", version="0.7.22"),
"Endpoint application mismatch",
),
@@ -3517,7 +3521,7 @@ def unreachable_binding() -> UiBridgeServerBinding:
raise AssertionError("invalid requests must not reach the bridge")
dispatcher = UiBridgeRequestDispatcher(
- UiAgentBridgeService(dispatcher=InlineDispatcher()),
+ OpenHCSUiAgentBridgeService(dispatcher=InlineDispatcher()),
auth_token=BRIDGE_AUTH_TOKEN,
binding_supplier=unreachable_binding,
)
@@ -3534,7 +3538,7 @@ def unreachable_binding() -> UiBridgeServerBinding:
response = dispatcher.dispatch(to_jsonable(request))
assert response["ok"] is False
- assert response["bridge_protocol_version"] == UI_BRIDGE_PROTOCOL_VERSION
+ assert response["bridge_protocol_version"] == OpenHCSUiBridgeApplication.protocol_version
assert response["application"] == to_jsonable(OPENHCS_ENDPOINT_APPLICATION)
assert message in response["errors"][0]["message"]
@@ -3697,7 +3701,7 @@ def _pipeline_editor_bridge(editor) -> UiAgentBridgeService:
snapshot_provider=snapshot_provider,
)
)
- return UiAgentBridgeService(
+ return OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -3705,7 +3709,7 @@ def _pipeline_editor_bridge(editor) -> UiAgentBridgeService:
def _plate_manager_bridge(manager, dispatcher=None) -> UiAgentBridgeService:
- return UiAgentBridgeService(
+ return OpenHCSUiAgentBridgeService(
provider_set=PlateManagerBridgeProviderSet(manager),
dispatcher=dispatcher or InlineDispatcher(),
)
@@ -3714,7 +3718,7 @@ def _plate_manager_bridge(manager, dispatcher=None) -> UiAgentBridgeService:
def _plate_manager_state(bridge, selection_mode=ALL_SELECTION_MODE, **kwargs):
return bridge.get_state_surface(
UiStateSurfaceRequest(
- surface_id=UiStateSurfaceId.PLATE_MANAGER.value,
+ surface_id=DatasetListStateSurfaceIdentity.require_value(),
selection_mode=selection_mode,
**kwargs,
)
@@ -3755,7 +3759,7 @@ def _main_window_bridge(main_window) -> UiAgentBridgeService:
snapshot_provider=snapshot_provider,
)
)
- return UiAgentBridgeService(
+ return OpenHCSUiAgentBridgeService(
registry=registry,
dispatcher=InlineDispatcher(),
snapshot_provider=snapshot_provider,
@@ -3951,7 +3955,7 @@ def test_plate_manager_state_surface_projects_runtime_row_status() -> None:
bridge, base_revision_token=state.current_revision_token
)
- assert state.summary.surface_id == UiStateSurfaceId.PLATE_MANAGER.value
+ assert state.summary.surface_id == DatasetListStateSurfaceIdentity.require_value()
row = state.payload["rows"][0]
assert row["scope_id"] == DATASET_SCOPE_ID
assert row["status_prefix"] == "⏳ Compile"
@@ -3980,8 +3984,8 @@ def test_every_dataset_action_relates_its_declared_state_surfaces() -> None:
)
)
assert actions[ShowLiveResults.operation_id].related_state_surface_ids == (
- "plate_manager.state",
- "plate_manager.live_measurements",
+ "datasets.state",
+ "datasets.live_measurements",
)
@@ -4208,7 +4212,7 @@ def test_selected_workflow_returns_before_the_queued_operation_runs(
assert result.action_result.status is ActionAccepted
assert operation_id is not None
assert result.action_result.workflow_status_surface_ids == (
- "plate_manager.state",
+ "datasets.state",
)
assert started == []
assert len(dispatcher.callbacks) == 1
@@ -4311,7 +4315,7 @@ def test_selected_workflow_confirmation_rejection_avoids_ui_preflight(
assert result.action_result.status is ActionRejected
assert result.errors[0].code == "confirmation_required"
assert result.action_result.workflow_status_surface_ids == (
- "plate_manager.state",
+ "datasets.state",
)
assert dispatcher.call_count == 0
assert started == []
@@ -4430,19 +4434,19 @@ def function_reference(function):
catalog = bridge.list_state_surfaces()
state = bridge.get_state_surface(
UiStateSurfaceRequest(
- surface_id=UiStateSurfaceId.PIPELINE_EDITOR.value,
+ surface_id=PipelineEditorStateSurfaceIdentityDeclaration.require_value(),
selection_mode=ALL_SELECTION_MODE,
)
)
selected_state = bridge.get_state_surface(
UiStateSurfaceRequest(
- surface_id=UiStateSurfaceId.PIPELINE_EDITOR.value,
+ surface_id=PipelineEditorStateSurfaceIdentityDeclaration.require_value(),
selection_mode=SELECTED_SELECTION_MODE,
base_revision_token=state.current_revision_token,
)
)
- assert catalog.surfaces[0].surface_id == UiStateSurfaceId.PIPELINE_EDITOR.value
+ assert catalog.surfaces[0].surface_id == PipelineEditorStateSurfaceIdentityDeclaration.require_value()
assert state.summary.current_selection_count == 1
assert state.summary.total_scope_count == 2
assert state.payload["pipeline_scope_id"] == (
@@ -4591,7 +4595,7 @@ def _debug_cursor() -> DebugCursor:
def _debug_state(gui):
return _pipeline_editor_bridge(gui.pipeline_editor).get_state_surface(
UiStateSurfaceRequest(
- surface_id=UiStateSurfaceId.PIPELINE_DEBUG_SESSION.value,
+ surface_id=PipelineDebugSessionStateSurfaceIdentityDeclaration.require_value(),
selection_mode=ALL_SELECTION_MODE,
)
)
@@ -4607,7 +4611,7 @@ def test_pipeline_debug_session_state_surface_projects_context_and_actions() ->
state = _debug_state(gui)
actions = {action["action_id"]: action for action in state.payload["actions"]}
- assert state.summary.surface_id == UiStateSurfaceId.PIPELINE_DEBUG_SESSION.value
+ assert state.summary.surface_id == PipelineDebugSessionStateSurfaceIdentityDeclaration.require_value()
assert state.payload["phase"] == "active_session"
assert state.payload["current_plate_scope_id"] == DATASET_SCOPE_ID
assert state.payload["pipeline_scope_id"] == (
diff --git a/tests/unit/pyqt_gui/test_ui_bridge_lifecycle_config.py b/tests/unit/pyqt_gui/test_ui_bridge_lifecycle_config.py
index a97b1da10d..6d10484281 100644
--- a/tests/unit/pyqt_gui/test_ui_bridge_lifecycle_config.py
+++ b/tests/unit/pyqt_gui/test_ui_bridge_lifecycle_config.py
@@ -8,7 +8,7 @@
import pytest
import zmq
-from pyqt_reactive.services.zmq_server_scan_service import ZMQServerScanService
+from formbridge.services.zmq_server_scan_service import ZMQServerScanService
from zmqruntime import (
ControlMessageType,
DataControlPortPairAuthority,
@@ -23,12 +23,9 @@
from python_introspect import to_jsonable
-from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import UiBridgeRequestEnvelope, UiBridgeStatus
-from openhcs.agent.services.ui_bridge_service import (
- UI_BRIDGE_PROTOCOL_VERSION,
- UiBridgeStatusOperation,
-)
+from formbridge.contracts import SCHEMA_VERSION
+from formbridge.bridge.dto import UiBridgeRequestEnvelope, UiBridgeStatus
+from formbridge.bridge.service import UiBridgeStatusOperation
from openhcs.pyqt_gui.config import (
AgentUiBridgeConfig,
PyQtGuiRuntimeContext,
@@ -39,13 +36,13 @@
from openhcs.pyqt_gui.services.ui_bridge_composition import (
OpenHCSUiBridgeCompositionRoot,
)
-from openhcs.pyqt_gui.services.ui_bridge_server import (
- UI_BRIDGE_BROWSER_SERVER_NAME,
+from formbridge.bridge.server import (
UiBridgeControlServer,
UiBridgeEndpointInUseError,
)
from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION
from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG, OpenHCSZMQConfig
+from openhcs.agent.ui_bridge_application import OpenHCSUiBridgeApplication
def test_agent_ui_bridge_config_reads_environment(monkeypatch) -> None:
@@ -468,7 +465,7 @@ def test_ui_bridge_answers_zmq_browser_control_ping(tmp_path) -> None:
context.term()
assert response["type"] == ResponseType.PONG.value
- assert response["server"] == UI_BRIDGE_BROWSER_SERVER_NAME
+ assert response["server"] == OpenHCSUiBridgeApplication.server_name
assert response["server_role"] == ServerRole.GENERIC.value
assert response["port"] == port
assert response["control_port"] == (port + OPENHCS_ZMQ_CONFIG.control_port_offset)
@@ -531,7 +528,7 @@ def test_second_ipc_ui_bridge_cannot_replace_live_endpoint_pair(
to_jsonable(
UiBridgeRequestEnvelope(
schema_version=SCHEMA_VERSION,
- bridge_protocol_version=UI_BRIDGE_PROTOCOL_VERSION,
+ bridge_protocol_version=OpenHCSUiBridgeApplication.protocol_version,
application=OPENHCS_ENDPOINT_APPLICATION,
request_id="identity",
operation=UiBridgeStatusOperation.name,
@@ -553,7 +550,7 @@ def test_second_ipc_ui_bridge_cannot_replace_live_endpoint_pair(
context.term()
assert (
- original_binding.connection.transport_endpoint().occupied_ports(
+ original_binding.connection.transport_endpoint(OPENHCS_ZMQ_CONFIG).occupied_ports(
transport_config
)
== frozenset()
@@ -597,7 +594,7 @@ def expiring_startup_lock(
return ExpiringStartupLock(operation_deadline)
monkeypatch.setattr(
- "openhcs.pyqt_gui.services.ui_bridge_server.endpoint_startup_lock",
+ "formbridge.bridge.server.endpoint_startup_lock",
expiring_startup_lock,
)
@@ -683,7 +680,7 @@ def test_zmq_browser_scan_service_discovers_ui_bridge_default_transport(
)
assert len(snapshot.responses) == 1
- assert snapshot.responses[0].server == UI_BRIDGE_BROWSER_SERVER_NAME
+ assert snapshot.responses[0].server == OpenHCSUiBridgeApplication.server_name
assert snapshot.responses[0].port == port
assert snapshot.responses[0].application == OPENHCS_ENDPOINT_APPLICATION
assert endpoint.occupied_ports(OPENHCS_ZMQ_CONFIG) == frozenset()
diff --git a/tests/unit/pyqt_gui/test_ui_bridge_live_measurements.py b/tests/unit/pyqt_gui/test_ui_bridge_live_measurements.py
index 49f3962893..8654844a47 100644
--- a/tests/unit/pyqt_gui/test_ui_bridge_live_measurements.py
+++ b/tests/unit/pyqt_gui/test_ui_bridge_live_measurements.py
@@ -4,14 +4,12 @@
from dataclasses import dataclass
from pathlib import Path
-from openhcs.core.selection import AllItems, SelectedItems
-from openhcs.agent.dto.ui_bridge import (
- UiStateSurfaceRequest,
-)
+from formbridge.selection import AllItems, SelectedItems
+from formbridge.bridge.dto import UiStateSurfaceRequest
from openhcs.agent.ui_bridge_identities import (
- PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration,
- UiStateSurfaceIdentityDeclarationBase,
+ DatasetLiveMeasurementsStateSurfaceIdentity,
)
+from formbridge.bridge.identities import UiStateSurfaceIdentityDeclarationBase
from openhcs.core.artifacts.kinds import MeasurementsArtifactType
from openhcs.core.component_group_scope import RuntimeExecutionAxisScope
from openhcs.core.progress import ProgressEvent
@@ -24,17 +22,13 @@
RuntimeArtifactAddress,
RuntimeArtifactLocation,
)
-from openhcs.pyqt_gui.services.ui_agent_bridge import UiAgentBridgeService
-from openhcs.pyqt_gui.services.ui_bridge_contracts import (
- state_surface_declaration_for_identity,
-)
-from openhcs.pyqt_gui.services.ui_bridge_live_overview import (
- LiveOverviewBridgeProviderSet,
-)
+from formbridge.bridge.in_process import UiAgentBridgeService
+from formbridge.bridge.contracts import state_surface_declaration_for_identity
+from openhcs.pyqt_gui.services.ui_bridge_live_overview import LiveOverviewBridgeProviderSet
from openhcs.pyqt_gui.services.ui_bridge_plate_manager import (
PlateManagerBridgeProviderSet,
)
-from openhcs.pyqt_gui.services.ui_bridge_registry import CompositeUiBridgeProviderSet
+from formbridge.bridge.registry import CompositeUiBridgeProviderSet
from openhcs.pyqt_gui.widgets.plate_manager import PlateManagerWidget
from openhcs.authoring.session.progress_notifications import (
LiveMeasurementAvailableNotification,
@@ -45,6 +39,7 @@
StepCompletedProgressPhase,
SuccessProgressStatus,
)
+from openhcs.pyqt_gui.services.ui_agent_bridge import OpenHCSUiAgentBridgeService
@dataclass(frozen=True, slots=True)
@@ -126,7 +121,7 @@ def _notification() -> LiveMeasurementAvailableNotification:
def _bridge(manager: _Manager) -> UiAgentBridgeService:
- return UiAgentBridgeService(
+ return OpenHCSUiAgentBridgeService(
provider_set=CompositeUiBridgeProviderSet(
(
PlateManagerBridgeProviderSet(manager),
@@ -140,17 +135,17 @@ def _bridge(manager: _Manager) -> UiAgentBridgeService:
def _live_measurement_declaration():
return state_surface_declaration_for_identity(
PlateManagerWidget.UI_STATE_SURFACE_DECLARATIONS,
- PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration,
+ DatasetLiveMeasurementsStateSurfaceIdentity,
)
def test_live_measurement_surface_is_declared_by_owning_widget() -> None:
declaration = _live_measurement_declaration()
- assert declaration.surface_id == "plate_manager.live_measurements"
+ assert declaration.surface_id == "datasets.live_measurements"
assert (
declaration.identity
- is PlateManagerLiveMeasurementsStateSurfaceIdentityDeclaration
+ is DatasetLiveMeasurementsStateSurfaceIdentity
)
assert declaration.payload_schema == "openhcs.ui.live_measurements_state.v1"
assert declaration.related_action_ids == (ShowLiveResults.operation_id,)
diff --git a/tests/unit/pyqt_gui/test_ui_bridge_live_overview.py b/tests/unit/pyqt_gui/test_ui_bridge_live_overview.py
index 083515e28e..d0337e56ea 100644
--- a/tests/unit/pyqt_gui/test_ui_bridge_live_overview.py
+++ b/tests/unit/pyqt_gui/test_ui_bridge_live_overview.py
@@ -2,9 +2,7 @@
from zmqruntime.viewer_state import ViewerInstance, ViewerState
-from openhcs.pyqt_gui.services.ui_bridge_live_overview import (
- ViewerSessionLiveOverviewContributor,
-)
+from openhcs.pyqt_gui.services.ui_bridge_live_overview import ViewerSessionLiveOverviewContributor
from openhcs.pyqt_gui.services.ui_window_ids import OpenHCSUiWindowId
diff --git a/tests/unit/pyqt_gui/test_ui_bridge_object_state_mutation.py b/tests/unit/pyqt_gui/test_ui_bridge_object_state_mutation.py
index 3f2544eb0e..59f9bd2c1b 100644
--- a/tests/unit/pyqt_gui/test_ui_bridge_object_state_mutation.py
+++ b/tests/unit/pyqt_gui/test_ui_bridge_object_state_mutation.py
@@ -3,11 +3,9 @@
from dataclasses import dataclass
from enum import Enum
-from openhcs.agent.dto.ui_bridge import UiObjectStateFieldMutationRequest
+from formbridge.bridge.dto import UiObjectStateFieldMutationRequest
from objectstate import ObjectState, ObjectStateRegistry
-from openhcs.pyqt_gui.services.ui_bridge_object_state import (
- ObjectStateFieldMutationService,
-)
+from formbridge.bridge.object_state import ObjectStateFieldMutationService
@dataclass
diff --git a/tests/unit/pyqt_gui/test_ui_bridge_preview_disclosure.py b/tests/unit/pyqt_gui/test_ui_bridge_preview_disclosure.py
index 5c578edc58..784d0e7a70 100644
--- a/tests/unit/pyqt_gui/test_ui_bridge_preview_disclosure.py
+++ b/tests/unit/pyqt_gui/test_ui_bridge_preview_disclosure.py
@@ -20,15 +20,8 @@
from pyqt_reactive.widgets.shared.styled_text_layout import StyledTextLayout, Segment
from openhcs.agent.dto.common import SCHEMA_VERSION
-from openhcs.agent.dto.ui_bridge import (
- UiWindowSummary,
- UiWindowIdentity,
- UiWidgetActionInvokeRequest,
-)
-from openhcs.pyqt_gui.services.ui_bridge_windows import (
- WindowProjectionTarget,
- UiWidgetActionInvokeResultFactory,
-)
+from formbridge.bridge.dto import UiWindowSummary, UiWindowIdentity, UiWidgetActionInvokeRequest
+from pyqt_reactive.bridge.windows import WindowProjectionTarget, UiWidgetActionInvokeResultFactory
@pytest.fixture
diff --git a/tests/unit/pyqt_gui/test_ui_config_lifecycle.py b/tests/unit/pyqt_gui/test_ui_config_lifecycle.py
index 36e182df47..f0a41bc57b 100644
--- a/tests/unit/pyqt_gui/test_ui_config_lifecycle.py
+++ b/tests/unit/pyqt_gui/test_ui_config_lifecycle.py
@@ -261,7 +261,7 @@ def test_removed_ui_config_and_lifecycle_mirrors_do_not_recur() -> None:
/ "config_cache_adapter.py"
).exists()
bridge_source = (
- repository_root / "openhcs" / "pyqt_gui" / "services" / "ui_bridge_server.py"
+ repository_root / "external" / "formbridge" / "src" / "formbridge" / "bridge" / "server.py"
).read_text()
assert "DEFAULT_UI_BRIDGE_HOST" not in bridge_source
assert "DEFAULT_UI_BRIDGE_TRANSPORT" not in bridge_source
diff --git a/tests/unit/pyqt_gui/test_ui_config_widget_projection.py b/tests/unit/pyqt_gui/test_ui_config_widget_projection.py
index 2a01ffd5ea..0ace84179c 100644
--- a/tests/unit/pyqt_gui/test_ui_config_widget_projection.py
+++ b/tests/unit/pyqt_gui/test_ui_config_widget_projection.py
@@ -29,7 +29,7 @@ def test_nested_ui_config_projects_color_enums_and_shortcut_capture(qapp) -> Non
ParameterFormManager,
)
from pyqt_reactive.protocols import KeySequenceEditAdapter
- from pyqt_reactive.services.system_monitor_config import (
+ from formbridge.services.system_monitor_config import (
PerformanceGraphColor,
PerformanceMonitorColors,
)
diff --git a/tests/unit/pyqt_gui/test_ui_window_flash_snapshot.py b/tests/unit/pyqt_gui/test_ui_window_flash_snapshot.py
index a91f70f080..65afec41c4 100644
--- a/tests/unit/pyqt_gui/test_ui_window_flash_snapshot.py
+++ b/tests/unit/pyqt_gui/test_ui_window_flash_snapshot.py
@@ -6,16 +6,17 @@
from PyQt6.QtCore import QEvent
from PyQt6.QtWidgets import QDialog, QVBoxLayout
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
UiBridgeOperationStatusRequest,
UiWindowSnapshotRequest,
UiWindowSnapshotResult,
)
-from openhcs.agent.services.ui_bridge_service import UiBridgeOperation
+from formbridge.bridge.service import UiBridgeOperation
from python_introspect import dataclass_from_mapping, to_jsonable
-from openhcs.pyqt_gui.services.ui_agent_bridge import UiAgentBridgeService
-from openhcs.pyqt_gui.services.ui_bridge_windows import QtTopLevelWindowProjection
-from pyqt_reactive.services.window_snapshot import WindowSnapshotFrameCondition
+from formbridge.bridge.in_process import UiAgentBridgeService
+from pyqt_reactive.bridge.windows import QtTopLevelWindowProjection
+from formbridge.services.window_snapshot import WindowSnapshotFrameCondition
+from openhcs.pyqt_gui.services.ui_agent_bridge import OpenHCSUiAgentBridgeService
@pytest.fixture
@@ -53,7 +54,7 @@ class Fields:
window.show()
qapp.processEvents()
projection = QtTopLevelWindowProjection(None)
- bridge = UiAgentBridgeService()
+ bridge = OpenHCSUiAgentBridgeService()
bridge.register_window_provider(projection)
window_id = projection.summary(window).window_id
yield bridge, form, window_id
@@ -198,8 +199,8 @@ def test_failed_observation_frame_and_existing_trace_decode_through_registered_o
native_snapshot_bridge,
tmp_path,
):
- from pyqt_reactive.flash_trace import FlashTraceRecord
- from pyqt_reactive.services.window_snapshot import FlashPaintFrame
+ from formbridge.flash_trace import FlashTraceRecord
+ from formbridge.services.window_snapshot import FlashPaintFrame
bridge, form, window_id = native_snapshot_bridge
accepted = bridge.snapshot_window(
@@ -261,14 +262,8 @@ def test_navigation_uses_existing_operation_with_terminal_native_driver_receipt(
native_snapshot_bridge,
prove_exposure,
):
- from openhcs.agent.dto.ui_bridge import (
- UiWindowNavigateRequest,
- UiWindowNavigateResult,
- )
- from openhcs.pyqt_gui.services.ui_bridge_windows import (
- UiWindowProjectionService,
- WindowRouteIndex,
- )
+ from formbridge.bridge.dto import UiWindowNavigateRequest, UiWindowNavigateResult
+ from pyqt_reactive.bridge.windows import UiWindowProjectionService, WindowRouteIndex
from pyqt_reactive.services.window_manager import WindowManager
from pyqt_reactive.services.window_navigation import FieldWindowNavigationDriver
@@ -326,7 +321,7 @@ def _route_index(self):
def test_unowned_target_navigation_is_terminal_failed_not_pending(
native_snapshot_bridge,
):
- from openhcs.agent.dto.ui_bridge import UiWindowNavigateRequest
+ from formbridge.bridge.dto import UiWindowNavigateRequest
bridge, _, window_id = native_snapshot_bridge
accepted = bridge.navigate_window(
@@ -344,7 +339,7 @@ def test_unowned_target_navigation_is_terminal_failed_not_pending(
def test_tracker_observation_failure_uses_declared_operation_error_owner(
native_snapshot_bridge,
):
- from openhcs.agent.services.ui_bridge_service import UiBridgeNavigateWindowOperation
+ from formbridge.bridge.service import UiBridgeNavigateWindowOperation
bridge, _, _ = native_snapshot_bridge
@@ -364,7 +359,7 @@ def rejected(completed):
def test_snapshot_tool_arguments_derive_capture_owner_fields(tmp_path):
from python_introspect import project_dataclass
- from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureSpec
+ from formbridge.services.window_snapshot import WindowSnapshotCaptureSpec
request = UiWindowSnapshotRequest.from_fields(
window_id="engineering-form",
diff --git a/tests/unit/pyqt_gui/test_user_facing_help_surfaces.py b/tests/unit/pyqt_gui/test_user_facing_help_surfaces.py
index 3d4d2c9ff0..730fe8ef45 100644
--- a/tests/unit/pyqt_gui/test_user_facing_help_surfaces.py
+++ b/tests/unit/pyqt_gui/test_user_facing_help_surfaces.py
@@ -6,7 +6,7 @@
from pathlib import Path
from PyQt6.QtWidgets import QApplication
-from pyqt_reactive.services.help_document import HelpDocumentFormat
+from formbridge.services.help_document import HelpDocumentFormat
from pyqt_reactive.services.window_manager import WindowManager
from pyqt_reactive.widgets.shared.clickable_help_components import HelpButton
from pyqt_reactive.windows.help_window_manager import HelpWindowManager
diff --git a/tests/unit/pyqt_gui/test_zmq_server_info.py b/tests/unit/pyqt_gui/test_zmq_server_info.py
index 8afe0a6115..7b3b93ee2a 100644
--- a/tests/unit/pyqt_gui/test_zmq_server_info.py
+++ b/tests/unit/pyqt_gui/test_zmq_server_info.py
@@ -1,4 +1,4 @@
-from pyqt_reactive.services.zmq_server_info import (
+from formbridge.services.zmq_server_info import (
BaseServerInfo,
ExecutionServerInfo,
GenericServerInfo,
diff --git a/tests/unit/test_build_website.py b/tests/unit/test_build_website.py
index 526f7f91f5..98a301ad47 100644
--- a/tests/unit/test_build_website.py
+++ b/tests/unit/test_build_website.py
@@ -13,7 +13,7 @@
import pytest
from openhcs import __version__ as OPENHCS_VERSION
-from openhcs.agent.ui_bridge_identities import UiStableWindowIdentityDeclaration
+from formbridge.bridge.identities import UiStableWindowIdentityDeclaration
from openhcs.resources.brand import (
BRAND_PRODUCT_NAME,
BrandAsset,
diff --git a/tests/unit/test_capture_media_gallery.py b/tests/unit/test_capture_media_gallery.py
index 1cefdf6cf8..8877666c7e 100644
--- a/tests/unit/test_capture_media_gallery.py
+++ b/tests/unit/test_capture_media_gallery.py
@@ -9,17 +9,16 @@
import pytest
-from openhcs.agent.services.ui_bridge_service import (
- UiBridgeSnapshotWindowOperation,
-)
+from formbridge.bridge.service import UiBridgeSnapshotWindowOperation
from pyqt_reactive.animation import WindowFlashOverlay
from pyqt_reactive.animation.flash_overlay_opengl import WindowFlashOverlayGL
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from python_introspect import dataclass_from_mapping, to_jsonable
-from openhcs.agent.dto.common import SCHEMA_VERSION, AgentResourceRef
+from openhcs.agent.dto.common import SCHEMA_VERSION
+from formbridge.contracts import AgentResourceRef
from openhcs.agent.dto.execution import ExecutionConnectionSpec
-from openhcs.agent.dto.ui_bridge import UiWindowSnapshotResult
+from formbridge.bridge.dto import UiWindowSnapshotResult
from openhcs.agent.dto.viewer import ViewerWindowDescriptor, ViewerWindowSnapshotResult
from openhcs.core.streaming_config_declarations import ViewerType
from scripts.capture_media_gallery import (
@@ -577,8 +576,8 @@ def test_shared_native_snapshot_ingestion_refuses_invalid_resource_without_movin
from pyqt_reactive.services.window_snapshot import (
QtWindowSnapshotRequest,
QtWindowSnapshotService,
- WindowSnapshotCaptureSpec,
)
+ from formbridge.services.window_snapshot import WindowSnapshotCaptureSpec
source_root = tmp_path / "capture"
request = GallerySourceCaptureRequest(
diff --git a/tests/unit/test_configuration_consumer_coverage.py b/tests/unit/test_configuration_consumer_coverage.py
index 4575214b63..1777b1bd69 100644
--- a/tests/unit/test_configuration_consumer_coverage.py
+++ b/tests/unit/test_configuration_consumer_coverage.py
@@ -139,6 +139,7 @@ def _production_syntax_units() -> tuple[_SyntaxUnit, ...]:
repository_root = Path(__file__).resolve().parents[2]
roots = (
repository_root / "openhcs",
+ repository_root / "external" / "formbridge" / "src",
repository_root / "external" / "pyqt-reactive" / "src",
repository_root / "external" / "zmqruntime" / "src",
repository_root / "external" / "arraybridge" / "src",
diff --git a/tests/unit/test_configuration_mirror_deletion.py b/tests/unit/test_configuration_mirror_deletion.py
index f182f47458..81bc5bf655 100644
--- a/tests/unit/test_configuration_mirror_deletion.py
+++ b/tests/unit/test_configuration_mirror_deletion.py
@@ -13,6 +13,7 @@
PROJECT_ROOT / "tests",
PROJECT_ROOT / "benchmark",
PROJECT_ROOT / "external/ObjectState/src",
+ PROJECT_ROOT / "external/formbridge/src",
PROJECT_ROOT / "external/pyqt-reactive/src",
PROJECT_ROOT / "external/python-introspect/src",
PROJECT_ROOT / "external/zmqruntime/src",
@@ -207,6 +208,7 @@ def test_deleted_lazy_registry_and_name_resolver_symbols_do_not_recur() -> None:
scanned_roots = (
PROJECT_ROOT / "openhcs",
PROJECT_ROOT / "external/ObjectState/src",
+ PROJECT_ROOT / "external/formbridge/src",
PROJECT_ROOT / "external/pyqt-reactive/src",
)
for root in scanned_roots:
@@ -320,11 +322,11 @@ def test_typed_transport_boundaries_do_not_regrow_payload_schema_helpers() -> No
def test_system_monitor_keeps_metrics_and_fixed_colors_typed() -> None:
scanned_paths = (
- PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/services/"
+ PROJECT_ROOT / "external/formbridge/src/formbridge/services/"
"system_metrics_sampler.py",
- PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/services/"
+ PROJECT_ROOT / "external/formbridge/src/formbridge/services/"
"system_monitor_config.py",
- PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/services/"
+ PROJECT_ROOT / "external/formbridge/src/formbridge/services/"
"system_monitor_core.py",
PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/services/"
"persistent_system_monitor.py",
@@ -365,9 +367,9 @@ def test_zmq_server_browser_keeps_pong_and_tree_identity_typed() -> None:
"ServerTree" + "Population",
}
scanned_paths = (
- PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/services/"
+ PROJECT_ROOT / "external/formbridge/src/formbridge/services/"
"zmq_server_info.py",
- PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/services/"
+ PROJECT_ROOT / "external/formbridge/src/formbridge/services/"
"zmq_server_scan_service.py",
PROJECT_ROOT / "external/pyqt-reactive/src/pyqt_reactive/widgets/shared/"
"zmq_server_browser_widget.py",
diff --git a/tests/unit/test_exception_traceback_compatibility.py b/tests/unit/test_exception_traceback_compatibility.py
index 198e073877..3cd08bf721 100644
--- a/tests/unit/test_exception_traceback_compatibility.py
+++ b/tests/unit/test_exception_traceback_compatibility.py
@@ -2,7 +2,7 @@
import pytest
-from openhcs.agent.services.ui_bridge_service import (
+from formbridge.bridge.service import (
UiBridgeDescriptorProcessGoneError,
UiBridgeDescriptorProcessIdentityError,
UiBridgeGatewayResponseError,
@@ -10,10 +10,8 @@
UiBridgeGatewayUnavailableError,
)
from openhcs.core.streaming_config_declarations import ViewerType
-from openhcs.pyqt_gui.services.ui_agent_bridge import UiCodeDocumentValidationError
-from openhcs.pyqt_gui.services.ui_bridge_server import (
- UiBridgeUnsupportedOperationError,
-)
+from formbridge.bridge.in_process import UiCodeDocumentValidationError
+from formbridge.bridge.server import UiBridgeUnsupportedOperationError
from openhcs.runtime.viewer_protocol import ViewerGraphicalSessionUnavailableError
diff --git a/tests/unit/test_formbridge_guards.py b/tests/unit/test_formbridge_guards.py
new file mode 100644
index 0000000000..27b38edd1c
--- /dev/null
+++ b/tests/unit/test_formbridge_guards.py
@@ -0,0 +1,129 @@
+"""U4 guards: headless code needs no GUI toolkit, and OpenHCS keeps only its own declarations.
+
+The toolkit-neutral tier (forms model, help, code documents, the running-UI
+bridge DTOs, contracts, client and server) lives in formbridge; pyqt-reactive
+holds the Qt window introspection; OpenHCS declares its application, identities,
+surfaces and operations.
+"""
+
+from __future__ import annotations
+
+import ast
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+REPOSITORY = Path(__file__).resolve().parents[2]
+PRODUCTION = REPOSITORY / "openhcs"
+FORMBRIDGE = REPOSITORY / "external" / "formbridge" / "src" / "formbridge"
+QT_BRIDGE = REPOSITORY / "external" / "pyqt-reactive" / "src" / "pyqt_reactive" / "bridge"
+TOOLKITS = ("PyQt6", "PyQt5", "PySide6", "PySide2", "qtpy")
+
+
+def test_headless_mcp_and_every_formbridge_module_load_no_toolkit() -> None:
+ script = f"""
+import builtins
+import importlib
+import pkgutil
+import sys
+
+toolkits = {TOOLKITS!r}
+original_import = builtins.__import__
+
+def reject_toolkit(name, globals=None, locals=None, fromlist=(), level=0):
+ if level == 0 and name.split(".")[0] in toolkits:
+ raise AssertionError(f"headless import loaded {{name}}")
+ return original_import(name, globals, locals, fromlist, level)
+
+builtins.__import__ = reject_toolkit
+import openhcs # a source checkout puts its external/ libraries on sys.path
+import formbridge
+for module in pkgutil.walk_packages(formbridge.__path__, "formbridge."):
+ importlib.import_module(module.name)
+from openhcs.mcp.server import build_server
+import openhcs.mcp.http
+build_server()
+loaded = sorted(name for name in sys.modules if name.split(".")[0] in toolkits)
+assert not loaded, loaded
+"""
+ completed = subprocess.run(
+ [sys.executable, "-c", script],
+ cwd=REPOSITORY,
+ capture_output=True,
+ text=True,
+ timeout=300,
+ )
+ assert completed.returncode == 0, completed.stderr[-4000:]
+
+
+def _top_level_classes(root: Path) -> dict[str, Path]:
+ classes = {}
+ for path in root.rglob("*.py"):
+ for node in ast.parse(path.read_text(encoding="utf-8")).body:
+ if isinstance(node, ast.ClassDef):
+ classes[node.name] = path
+ return classes
+
+
+def test_openhcs_defines_no_class_the_libraries_own() -> None:
+ library = {**_top_level_classes(FORMBRIDGE), **_top_level_classes(QT_BRIDGE)}
+ restated = sorted(
+ f"{path.relative_to(REPOSITORY)}: {name}"
+ for name, path in _top_level_classes(PRODUCTION).items()
+ if name in library
+ )
+ assert restated == []
+
+
+def test_libraries_name_no_openhcs_module() -> None:
+ offenders = sorted(
+ f"{path.relative_to(REPOSITORY)}:{node.lineno}"
+ for root in (FORMBRIDGE, QT_BRIDGE)
+ for path in root.rglob("*.py")
+ for node in ast.walk(ast.parse(path.read_text(encoding="utf-8")))
+ if isinstance(node, ast.ImportFrom | ast.Import)
+ and any(
+ (alias.name if isinstance(node, ast.Import) else node.module or "")
+ .split(".")[0]
+ == "openhcs"
+ for alias in node.names
+ )
+ )
+ assert offenders == []
+
+
+def test_bridge_constants_and_identity_enums_stay_deleted() -> None:
+ deleted = (
+ "UI_BRIDGE_PROTOCOL_VERSION",
+ "UiBridgeDescriptorEnvironment",
+ "_identity_enum",
+ "UiCodeDocumentId",
+ "UiStateSurfaceId",
+ "UiWidgetId",
+ "enum_member_name",
+ "_current_function_spec",
+ )
+ offenders = sorted(
+ f"{path.relative_to(REPOSITORY)}: {name}"
+ for root in (PRODUCTION, FORMBRIDGE, QT_BRIDGE)
+ for path in root.rglob("*.py")
+ for name in deleted
+ if re.search(rf"\b{name}\b", path.read_text(encoding="utf-8"))
+ )
+ assert offenders == []
+
+
+def test_moved_modules_do_not_exist_in_openhcs() -> None:
+ moved = (
+ "core/selection.py",
+ "agent/runtime_platform.py",
+ "agent/services/ui_bridge_transport.py",
+ "agent/services/object_state_field_projection.py",
+ "pyqt_gui/services/ui_bridge_contracts.py",
+ "pyqt_gui/services/ui_bridge_registry.py",
+ "pyqt_gui/services/ui_bridge_server.py",
+ "pyqt_gui/services/ui_bridge_object_state.py",
+ "pyqt_gui/services/ui_bridge_object_state_scope_policy.py",
+ )
+ assert [path for path in moved if (PRODUCTION / path).exists()] == []
diff --git a/tests/unit/test_function_patterns.py b/tests/unit/test_function_patterns.py
index 6905790764..b4f41355e4 100644
--- a/tests/unit/test_function_patterns.py
+++ b/tests/unit/test_function_patterns.py
@@ -436,7 +436,7 @@ def measure(image):
"sigma": 1,
"enabled": True,
"dtype_config": "inherited",
- "__pyqt_reactive_scope_token__": "ui",
+ "__formbridge_scope_token__": "ui",
},
),
measure,
diff --git a/tests/unit/test_mcp_assay_showcase.py b/tests/unit/test_mcp_assay_showcase.py
index d4acf5aa45..e8bc7d9b3f 100644
--- a/tests/unit/test_mcp_assay_showcase.py
+++ b/tests/unit/test_mcp_assay_showcase.py
@@ -363,10 +363,10 @@ def test_live_measurement_surface_is_discovered_from_public_catalog():
{
"actions": [
{
- "widget_id": "plate_manager",
+ "widget_id": "datasets",
"action_id": "show_live_results",
"related_state_surface_ids": [
- "plate_manager.state",
+ "datasets.state",
"runtime-owned-id",
],
},
@@ -374,7 +374,7 @@ def test_live_measurement_surface_is_discovered_from_public_catalog():
},
{
"surfaces": [
- {"surface_id": "plate_manager.state"},
+ {"surface_id": "datasets.state"},
{"surface_id": "runtime-owned-id"},
]
},
@@ -671,7 +671,7 @@ def execute(self, argv, *, timeout_seconds):
"surfaces": [
{
"surface_id": "runtime-owned-id",
- "widget_id": "plate_manager",
+ "widget_id": "datasets",
"title": "Live measurement results",
}
]
@@ -683,10 +683,10 @@ def execute(self, argv, *, timeout_seconds):
{
"actions": [
{
- "widget_id": "plate_manager",
+ "widget_id": "datasets",
"action_id": "show_live_results",
"related_state_surface_ids": [
- "plate_manager.state",
+ "datasets.state",
"runtime-owned-id",
],
}
@@ -867,6 +867,6 @@ def test_showcase_reuses_one_persistent_session_and_writes_timing_report(tmp_pat
argv[:2] == ("state-surface", "runtime-owned-id") for argv in fake_client.argv
)
assert any(
- argv[:3] == ("invoke-action", "plate_manager", "show_live_results")
+ argv[:3] == ("invoke-action", "datasets", "show_live_results")
for argv in fake_client.argv
)
diff --git a/tests/unit/test_mcp_thesis_demo_live.py b/tests/unit/test_mcp_thesis_demo_live.py
index 741c96b8c9..e122c9d4e3 100644
--- a/tests/unit/test_mcp_thesis_demo_live.py
+++ b/tests/unit/test_mcp_thesis_demo_live.py
@@ -1052,7 +1052,7 @@ def test_plate_action_waits_for_accepted_receipt(monkeypatch) -> None:
lambda _ctx, _action: demo.UiActionSummary(
schema_version="openhcs.agent.v1",
identity=demo.UiActionIdentity(
- widget_id="plate_manager", action_id=action.operation_id
+ widget_id="datasets", action_id=action.operation_id
),
title="Action",
enabled=True,
diff --git a/tests/unit/test_napari_render_snapshot.py b/tests/unit/test_napari_render_snapshot.py
index b5c30fc627..c507e95511 100644
--- a/tests/unit/test_napari_render_snapshot.py
+++ b/tests/unit/test_napari_render_snapshot.py
@@ -11,7 +11,7 @@
from qtpy.QtCore import QEvent, Signal
from qtpy.QtGui import QColor, QImage, QPainter
from qtpy.QtWidgets import QApplication, QWidget
-from pyqt_reactive.services.window_snapshot import WindowSnapshotFrameCondition
+from formbridge.services.window_snapshot import WindowSnapshotFrameCondition
from zmqruntime.config import TransportMode
from zmqruntime.transport import TransportEndpoint
from zmqruntime.timeouts import OperationDeadline
diff --git a/tests/unit/test_process_environment.py b/tests/unit/test_process_environment.py
index 73a0f47071..3918281ec6 100644
--- a/tests/unit/test_process_environment.py
+++ b/tests/unit/test_process_environment.py
@@ -5,7 +5,7 @@
ImageJArchiveDownloadPolicy,
)
-from openhcs.agent.runtime_platform import WindowsRuntimePlatform
+from formbridge.runtime_platform import WindowsRuntimePlatform
from openhcs.resources.brand import BRAND_PRODUCT_NAME
from openhcs.utils.environment import OpenHCSProcessEnvironment
diff --git a/tests/unit/test_smoke_installed_gui.py b/tests/unit/test_smoke_installed_gui.py
index e75a869b2d..5576225472 100644
--- a/tests/unit/test_smoke_installed_gui.py
+++ b/tests/unit/test_smoke_installed_gui.py
@@ -7,7 +7,7 @@
from types import SimpleNamespace
import pytest
-from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
+from formbridge.services.window_snapshot import WindowSnapshotCaptureScope
from zmqruntime import (
DataControlPortPair,
DataControlPortPairAuthority,
@@ -16,10 +16,10 @@
TransportMode,
)
-from openhcs.agent.dto.common import AgentResourceRef
+from formbridge.contracts import AgentResourceRef
from openhcs.agent.dto.execution import ExecutionConnectionSpec, RuntimeServerInfo
from openhcs.agent.dto.functions import FunctionCatalogEntry, FunctionCatalogPage
-from openhcs.agent.dto.ui_bridge import (
+from formbridge.bridge.dto import (
ActionAccepted,
UiActionInvocationStatus,
UiWindowSnapshotResult,
diff --git a/tests/unit/test_time_travel_navigation.py b/tests/unit/test_time_travel_navigation.py
index f50d36f49f..1a4ad25218 100644
--- a/tests/unit/test_time_travel_navigation.py
+++ b/tests/unit/test_time_travel_navigation.py
@@ -2,7 +2,7 @@
from objectstate.object_state import ObjectState, ObjectStateRegistry
from openhcs.core.steps.function_step import FunctionStep
from openhcs.pyqt_gui.ui_tab_identities import DualEditorTab
-from pyqt_reactive.services.function_navigation import build_function_token_field_path
+from formbridge.services.function_navigation import build_function_token_field_path
from pyqt_reactive.services.window_navigation import (
NavigationWaitReason,
RegisteredWindowNavigationReadiness,
diff --git a/tests/unit/test_viewer_receipt_replay.py b/tests/unit/test_viewer_receipt_replay.py
index 4b0efd499b..ac8eb5e41b 100644
--- a/tests/unit/test_viewer_receipt_replay.py
+++ b/tests/unit/test_viewer_receipt_replay.py
@@ -20,7 +20,8 @@
from polystore.streaming_constants import StreamingDataType
from zmqruntime.viewer_protocol import ViewerNativeLayerTransform
-from openhcs.agent.dto.common import AgentResourceRef, SCHEMA_VERSION
+from formbridge.contracts import AgentResourceRef
+from openhcs.agent.dto.common import SCHEMA_VERSION
from openhcs.agent.dto.execution import ExecutionConnectionSpec
from openhcs.agent.dto.plate import PlateFileStreamRequest
from openhcs.agent.dto.plate import PlateFileStreamResult
diff --git a/website/assets/gallery/release-media-record.json b/website/assets/gallery/release-media-record.json
index acd6bd7225..399b822243 100644
--- a/website/assets/gallery/release-media-record.json
+++ b/website/assets/gallery/release-media-record.json
@@ -452,7 +452,7 @@
]
},
{
- "id": "ui-plate-manager",
+ "id": "ui-datasets",
"publication_targets": [
"documentation"
],
@@ -461,7 +461,7 @@
"kind": "ui_bridge_window",
"human_review_required": false,
"parameters": {
- "window_id": "plate_manager",
+ "window_id": "datasets",
"create_if_missing": true
}
},
@@ -477,7 +477,7 @@
{
"role": "image",
"media_type": "image/webp",
- "path": "ui-plate-manager.webp",
+ "path": "ui-datasets.webp",
"sha256": "09a340b7b6320827429ca67e22b35aa39ed9543424be07bae399de881201d5b3"
}
]
diff --git a/website/assets/gallery/source-capture-evidence.json b/website/assets/gallery/source-capture-evidence.json
index ffd79ddf62..0bacf2e56b 100644
--- a/website/assets/gallery/source-capture-evidence.json
+++ b/website/assets/gallery/source-capture-evidence.json
@@ -81,7 +81,7 @@
}
},
{
- "scenario_id": "ui-plate-manager",
+ "scenario_id": "ui-datasets",
"source": {
"duration_seconds": null,
"format": "png",
diff --git a/website/assets/gallery/ui-plate-manager.webp b/website/assets/gallery/ui-datasets.webp
similarity index 100%
rename from website/assets/gallery/ui-plate-manager.webp
rename to website/assets/gallery/ui-datasets.webp