From 4af6a10fca3a7544adc5c57fc19196c1f9f5421c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 00:21:12 -0400 Subject: [PATCH 01/10] refactor(mcp): own pipeline result descent and presentation by declarations --- docs/refactor/01-INDEX.md | 4 + docs/refactor/S1-mcp-result-owners.rst | 186 +++++ openhcs/agent/capabilities.py | 54 +- openhcs/agent/dto/execution.py | 5 +- openhcs/agent/dto/pipeline.py | 6 +- openhcs/mcp/dev_client.py | 12 +- openhcs/mcp/dev_client_commanding.py | 25 +- .../dev_client_commands/knowledge_pipeline.py | 49 +- openhcs/mcp/dev_client_core.py | 203 ++++- openhcs/mcp/dev_client_renderers/knowledge.py | 8 +- openhcs/mcp/dev_client_renderers/pipeline.py | 730 ++++++++---------- openhcs/mcp/dev_client_renderers/ui_bridge.py | 21 +- openhcs/mcp/dev_client_rendering.py | 126 ++- .../test_mcp_dev_client_pipeline_results.py | 566 ++++++++++++++ tests/unit/agent/test_mcp_server.py | 40 +- 15 files changed, 1502 insertions(+), 533 deletions(-) create mode 100644 docs/refactor/S1-mcp-result-owners.rst create mode 100644 tests/unit/agent/test_mcp_dev_client_pipeline_results.py diff --git a/docs/refactor/01-INDEX.md b/docs/refactor/01-INDEX.md index 33caba08b8..a13794b3cc 100644 --- a/docs/refactor/01-INDEX.md +++ b/docs/refactor/01-INDEX.md @@ -99,3 +99,7 @@ NRA's complete scan of the package is running and will be added to the evidence Written just in time: [`R0-stop-the-inflow.md`](R0-stop-the-inflow.md), and the authoritative [L0 pattern-resolver checkpoint](L0-pattern-resolver.rst). The latter closes one confirmed dead module, not the whole L0 surface. + +Current [S1 MCP result-owner surface receipt](S1-mcp-result-owners.rst) records +the coherent pipeline checkpoint and named remaining renderer families; it does +not close the full S1 surface or installed acceptance. diff --git a/docs/refactor/S1-mcp-result-owners.rst b/docs/refactor/S1-mcp-result-owners.rst new file mode 100644 index 0000000000..003afa7edb --- /dev/null +++ b/docs/refactor/S1-mcp-result-owners.rst @@ -0,0 +1,186 @@ +S1: MCP development-client result ownership +=========================================== + +Source owner: Codex S1. Integration owner: parent. Audited canonical main: +e690c3bfc0f2042dcc2c75e6205d03fffe8aa603. Branch: +refactor/mcp-result-owners-s1-20261001. Worktree: +/home/ts/wt/openhcs-mcp-result-owners-s1-20261001. + +Binding instructions read completely before decisions: S1-DISPATCH-20261001.rst, +GOAL-SCOPE-REFRACTOR-20260930.md, canonical 00-RULES.md and 01-INDEX.md, +OWNER-OVERRIDES.md, both NRA/refactor-audit SKILL.md files, the authoritative +NRA skills/refactor-audit.skill archive SKILL.md, patterns README, complete +boundaries, implementation, membership, identity and over-time patterns, +surface-receipt instructions and NRA batching instructions. + +Before-source measurement and antipattern review +------------------------------------------------ + +Original debt_census.py, existing Python 3.14.7, snapshot at the audited head: +8 renderer modules, 6068 code lines; literal_key_get 166.61/1000 (1011 reads), +isinstance_call 20.60/1000 (125), none_identity 27.03/1000 (164). +pipeline.py has 502 code lines and weighted debt 100. Snapshot, not a zero diff. + +Explicit antipattern review confirmed from current source: + +* BOUND-2: pipeline.py:207 declares ArtifactPlanInspection, while :216-241 + read its fields as raw keys and :247-289 read SourceWorkspaceSummary and + SourceWorkspaceFileRecord as raw records. execution.py DTO declarations own + these fields, including nested materialization and streaming owners. +* BOUND-1/IMPL-12/IMPL-13: draft :27-43 and execution :534-543 repeatedly + project tool envelopes through raw helpers; field validation differs among + nested_mapping, optional_bool/int and sequence_of_mappings. The existing + python_introspect/dataclass_projection.py codec already performs recursive + annotation-owned reconstruction and strict rejection. Reuse it, not copies. +* MEMB-1/2/4: dev_client_rendering.py:129-209 and capability declarations own + renderer/output membership through AutoRegisterMeta and render_bindings. + Extend that owner; do not introduce a decoder catalog or name/type switches. +* MEMB-5/TIME-9: McpDevToolResult and McpDevToolBatchResponse already own framing + at dev_client_core.py:393 and :670. No replacement envelope or DTO facade. + Their single payload sequence must carry decoded values, not a parallel store. +* IDEN-1/3/5: absent tool, missing payload, malformed contract and agent failure + are distinct observations. Keep transport/agent diagnostics and original + malformed receipt, not an empty successful record. No duplicated payload cache. +* IMPL-14: strict field validation belongs in the existing generic codec, not + new anonymous field/key conditions in renderers. Errors identify their path. +* BOUND-4: draft repair guidance currently parses a human missing-kwargs message + (:156-192). Preserve current user guidance; do not claim it is structured proof. + +Producer/consumer trace and required relation +--------------------------------------------- + +Stdio/socket JSON RPC request -> _content_payloads (structuredContent or MCP +text content) -> McpDevToolResult.from_payload -> command run_session -> batch -> +compact command rendering / external JSON serialization. Native source producer +DTOs -> to_jsonable -> MCP content are unchanged. Existing capability output +declarations select the renderer binding; that binding owns payload reconstruction +through the existing codec. Typed renderer members read actual DTO fields. + +ExecutionJobRecord.status at execution_session_service.py:537-558 returns the +existing richer ExecutionJobStatus. _submit_job at :1210-1265 returns +ExecutionJobRef or ExecutionJobStatus (wait and submission failure), despite +capabilities.py:2712/:2743 advertising only ExecutionJobRef. S1 owns a reproducer +and full-receipt decoding for this source presentation mismatch. The implemented +ExecutionSubmissionResult union belongs to the existing execution declaration; +AgentResultFamilyContract extends existing capability output declarations with +that actual family and keeps the original advertised ExecutionJobRef metadata. +No presentation-only alternatives or replacement submission DTO. Native producer +bytes and advertised metadata remain unchanged. Both real siblings render through +ExecutionJobRenderer's ExecutionJobIdentity-owned common procedure. + +Initial coherent checkpoint +---------------------------- + +Migrate pipeline draft, validation, rendered source, artifact plan and execution +as one command family. Extend existing renderer binding and framing owners; +preserve one JSON serialization path. Delete this family's old raw projections, +per-field checks and nested record readers in place. Real command consumers use +the same decoded values as ingress; serialized command input is decoded at its +boundary, not via an old reader. Diagnostics retain full receipts; compact +display limits do not modify the stored result. Genuine JSON extension metadata +and native execution response bags remain mappings. + +No stored format changes, migrations, numeric processing or scientific artifacts. +MCP text/structured content and existing native DTO serialization remain external +and unchanged. No package, configured-skill or managed-blind instruction changes. + +New-case decisions +------------------ + +Before: a new nested source/plan fact requires a DTO declaration plus another +raw reader and validation at each consumer. After: fields decode from their true +owner; a renderer adds presentation only. A new capability returning a registered +output contract needs no decoder or caller change. Test that extension through +AutoRegisterMeta. Compose MI only for demonstrated overlapping capabilities; +there is no evidence requiring new MI in the initial pipeline family. + +Current source checkpoint +------------------------- + +Parent's three concrete pre-checkpoint ownership risks were reviewed against +source, not treated as a completed parent review. The presentation-only Status +retry binding was deleted; producer union membership is now the capability fact. +Status no longer inherits the Ref renderer. Both share the actual identity owner. +has_errors/agent_error_codes no longer convert typed results to JSON and rescan +raw records: one declaration-driven diagnostic traversal handles typed AgentError +identity, typed dataclass fields and genuine external JSON extension metadata. +Malformed ingress retains one original receipt with typed boundary diagnoses. + +Deleted mechanisms: pipeline family's literal-key field roster and type checks; +its repeated nested workspace/artifact/materialization readers; draft/session +consumers' raw extraction; duplicate renderer-membership tuple and registration +store; copied raw diagnostic scans; cross-family source truncation procedure. +Shared source clipping now belongs to CodeDocumentRenderOptions. Composite +command rendering belongs to TypedCompositeCommandSpec; per-case presentation +belongs to actual declared output renderers. Existing generic dataclass codec and +existing AutoRegisterMeta registry are the only reconstruction/membership owners. +The registry key inheritance defect is fixed at its existing nominal owner: +subclass registration projects only its declaration, not its parent's assigned +key. MRO lookup reuses ancestor behavior without pretending sibling DTO subtyping. + +New-case evidence: ExtendedInspection adds one DTO declaration and inherits +ArtifactPlanInspection behavior with its extra fact retained. A declared Diamond +renderer composes two independent cooperative presentation capabilities; each +executes once, identity occurs once in the hierarchy view, and AutoRegisterMeta +contains its declared output key. No consumer, dispatcher or field roster edits. + +Validation and resource ownership +--------------------------------- + +Run original packaged R0 and unchanged scripts/check_refactor_r1.py, no exceptions +or changed guard inputs. Provider-free family tests must drive production command +rendering and transport-content reconstruction with real nested DTOs, diagnostics, +progress, malformed/missing/not-run and a declaration extension. Preserve original +failed shard results before splitting or repairing incomplete internal fixtures. +Existing interpreters/dependencies only; serial shards <=512 MiB, one CPU, 60s. + +Initial new production-path shard: 14 passed, 5.28s wall, 250284 KiB RSS. +Combined coherent pipeline checkpoint: 25 passed (18 new, seven existing), +11.14s wall, 312636 KiB RSS, one CPU. New coverage drives real run_session, +call_mcp_tool framing and command rendering; text/structured content, nested +diagnostics/hints, source limits, native Ref/Status branches, progress, extension +metadata, malformed nested/missing/undeclared/bool-as-int shapes, absent/not-run, +transport/MCP failures, exact malformed receipt retention and actual CLI main's +compact/JSON entrypoints with a controlled wire response, no runtime launch. +Native server metadata remains exactly openhcs/outputContract=ExecutionJobRef. +Typed error interpretation is tested with serialization forbidden. + +Original failures remain in the owned output archive: source-first import order +collection failure; JsonValue annotation namespace failure; scoped registry/MRO +failures; five existing fixture failures. Existing fixture updates supply missing +real server/schema/config/ref/source-identity/output-dir fields, remove nonexistent +errors on PipelineSpec/RenderedSource and replace nonexistent base_path with +shared_output_stem. No behavior assertion was removed or weakened. Two actual +presentation regressions (Validate errors label and None group text) were fixed. + +Source imports needed the checkout's declared C++ extensions. Built only this +worktree's extensions with existing setup.py, compiler and dependency objects, +one build worker: 3.29s/143740 KiB. No interpreter/package/install changes. +Recorded submodules initialized only here at existing gitlinks; no gitlink edits. +Own compiler/build-lib scratch and two local abi3 binaries are disposable and +must be cleaned after evidence is archived. No application native launch. + +Resource helper before work reports warning/exit 2: swap used 14.2 GiB, available +RAM 20.0 GiB, /home free 23.1 GiB. No extra agents or large tests authorized while +this persists; use only serial bounded focused checks. Owned disposable output: +/home/ts/.cache/agent-scratch/openhcs-mcp-result-owners-s1-20261001, purpose source +census/guard/test logs, owner Codex S1. Archive useful receipts in this worktree, +then remove that exact owned run directory after termination. No volatile source. + +Source-context scan must include OpenHCS production and recorded dependency +sources where safe. Any budget/tool limitation is incomplete coverage, not global +proof or a blocker to independent implementation. No held-out/reference/science +data is read. No native/MCP/JVM/viewer launches. + +Remaining S1 and installed acceptance +------------------------------------- + +Codex S1 retains plate, viewer/runtime, UI/code-document/state-surface, +object-state, knowledge/function/authoring and config families after the initial +pipeline checkpoint. XML329, runtime/BaSiCPy217/326 and canonical skill references +remain with their existing owners. Current PR331 core admission is also disjoint. +No parent/foreign/main worktree edits. Parent merges useful locally verified draft +checkpoints with CI deferred. Parent owns affected installed CLI/MCP user journeys +after the scientific lock release. A source import/help/render check is not +installed or live acceptance. Neither this checkpoint nor a partial NRA scan +closes S1, the full ZIP or the parent's scientific goal. diff --git a/openhcs/agent/capabilities.py b/openhcs/agent/capabilities.py index 8ef1b2c7c4..853cc77a24 100644 --- a/openhcs/agent/capabilities.py +++ b/openhcs/agent/capabilities.py @@ -11,7 +11,8 @@ from importlib.metadata import distributions from inspect import getdoc from math import isfinite -from typing import ClassVar, Generic, Self, TypeAlias, TypeVar +from types import UnionType +from typing import ClassVar, Generic, Self, TypeAlias, TypeVar, get_args from metaclass_registry import AutoRegisterMeta from zmqruntime.client import EndpointShutdownResult @@ -45,6 +46,7 @@ ExecutionJobCancellationResult, ExecutionJobRef, ExecutionJobStatus, + ExecutionSubmissionResult, ExecutionStatusRequest, OrchestratorSession, OrchestratorSessionCreationRequest, @@ -611,7 +613,28 @@ def schema_name(self) -> str: return self.field_name -AgentContract: TypeAlias = type | AgentScalarInputContract +@dataclass(frozen=True, slots=True) +class AgentResultFamilyContract: + """Actual producer alternatives and their existing external nominal identity. + + MCP's advertised owner is an external metadata fact, not the entire result + family. Keep those questions distinct while deriving decode membership from + the producer's declared union. No presentation roster owns that membership. + """ + + advertised_contract: type + result_type: UnionType + + @property + def result_types(self) -> tuple[type, ...]: + return get_args(self.result_type) + + @property + def schema_name(self) -> str: + return self.advertised_contract.__name__ + + +AgentContract: TypeAlias = type | AgentScalarInputContract | AgentResultFamilyContract AgentContextT = TypeVar("AgentContextT") AgentServiceT = TypeVar("AgentServiceT") AgentRequestT = TypeVar("AgentRequestT") @@ -635,12 +658,14 @@ def _enum_member_title(value: Enum) -> str: def _contract_schema_name(contract: AgentContract | None) -> str | None: if contract is None: return None - if isinstance(contract, AgentScalarInputContract): + if isinstance(contract, (AgentScalarInputContract, AgentResultFamilyContract)): return contract.schema_name return contract.__name__ def require_agent_type_contract(contract: AgentContract | None) -> type: + if isinstance(contract, AgentResultFamilyContract): + return contract.advertised_contract if not isinstance(contract, type): raise TypeError(f"Expected agent type contract, got {contract!r}.") return contract @@ -958,6 +983,17 @@ class AgentCapabilitySpec: progress_worker_thread_safe: bool = True input_contract: AgentContract | None = None output_contract: AgentContract | None = None + + @property + def output_contract_types(self) -> tuple[type, ...]: + if isinstance(self.output_contract, AgentResultFamilyContract): + return self.output_contract.result_types + return ( + () + if self.output_contract is None + else (require_agent_type_contract(self.output_contract),) + ) + exposition: AgentCapabilityExposition | None = None def __post_init__(self) -> None: @@ -2709,7 +2745,9 @@ class SubmitCompileCapability(HeadlessExecutionCapability): mutating = True side_effects = ("submits_zmq_compile_job",) input_contract = CompileSubmissionRequest - output_contract = ExecutionJobRef + output_contract = AgentResultFamilyContract( + ExecutionJobRef, ExecutionSubmissionResult + ) request_invocation = AgentDataclassRequestServiceInvocation( service=lambda context: context.execution_service, method=lambda service, request: service.submit_compile( @@ -2740,7 +2778,9 @@ class SubmitPipelineExecutionCapability(HeadlessExecutionCapability): mutating = True side_effects = ("submits_zmq_execution_job",) input_contract = PipelineExecutionSubmissionRequest - output_contract = ExecutionJobRef + output_contract = AgentResultFamilyContract( + ExecutionJobRef, ExecutionSubmissionResult + ) request_invocation = AgentDataclassRequestServiceInvocation( service=lambda context: context.execution_service, method=lambda service, request: service.submit_execution( @@ -2833,7 +2873,9 @@ class ObserveOwnedRuntimeCapability(RuntimeServerCliConnectionCapability): service = "runtime_server" input_contract = RuntimeBootstrapObserveRequest output_contract = RuntimeBootstrapState - exposition = StartOwnedRuntimeCapability.exposition.refine(workflow_stage=CapabilityWorkflowStage.STATUS) + exposition = StartOwnedRuntimeCapability.exposition.refine( + workflow_stage=CapabilityWorkflowStage.STATUS + ) request_invocation = AgentDataclassRequestServiceInvocation( service=lambda context: context.runtime_server_service, method=lambda service, request: service.observe_bootstrap(request), diff --git a/openhcs/agent/dto/execution.py b/openhcs/agent/dto/execution.py index 3205d01285..dc2273b33c 100644 --- a/openhcs/agent/dto/execution.py +++ b/openhcs/agent/dto/execution.py @@ -6,7 +6,7 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass, field, replace from functools import wraps -from typing import Self +from typing import Self, TypeAlias from python_introspect import validate_annotated_dataclass from zmqruntime.config import ( @@ -310,6 +310,9 @@ def is_terminal(self) -> bool: return lifecycle_status is not None and lifecycle_status.is_terminal +ExecutionSubmissionResult: TypeAlias = ExecutionJobRef | ExecutionJobStatus + + @dataclass(frozen=True, slots=True) class ExecutionJobCancellationResult(AgentResultEnvelope): """One cancellation attempt and the ordinary job status observed afterward.""" diff --git a/openhcs/agent/dto/pipeline.py b/openhcs/agent/dto/pipeline.py index 78f3a4811e..c9e989db03 100644 --- a/openhcs/agent/dto/pipeline.py +++ b/openhcs/agent/dto/pipeline.py @@ -5,7 +5,11 @@ from collections.abc import Mapping from dataclasses import dataclass, field -from openhcs.agent.dto.common import AgentResultEnvelope, JsonObject +from openhcs.agent.dto.common import ( + AgentResultEnvelope, + JsonObject, + JsonValue as JsonValue, +) from openhcs.agent.dto.config import ConfigPatch from openhcs.agent.dto.functions import FunctionIdentity diff --git a/openhcs/mcp/dev_client.py b/openhcs/mcp/dev_client.py index e041a7cd15..61d146b27d 100644 --- a/openhcs/mcp/dev_client.py +++ b/openhcs/mcp/dev_client.py @@ -203,11 +203,11 @@ def execute( server_stderr_tail=captured_server_stderr_tail(self._server_stderr), ) payload = require_json_object_payload(to_jsonable(response)) - returncode = 0 if args.allow_error_payloads else int(_command_failed(payload)) + returncode = 0 if args.allow_error_payloads else int(response.has_errors()) return McpDevCommandExecution( argv=normalized_argv, payload=payload, - rendered_output=command_spec.render_response(payload, args), + rendered_output=command_spec.render_result(response, args), returncode=returncode, server_stderr_tail=captured_server_stderr_tail(self._server_stderr), ) @@ -482,16 +482,14 @@ def main(argv: Sequence[str] | None = None) -> int: return _run_persistent_shell(args) command_spec = McpDevCommandSpec.for_name(args.command) try: - payload = require_json_object_payload( - to_jsonable(asyncio.run(_run_async(args))) - ) + response = asyncio.run(_run_async(args)) except McpDevCliUsageError as exc: parser.error(str(exc)) - if not write_stdout(command_spec.render_response(payload, args)): + if not write_stdout(command_spec.render_result(response, args)): return 0 if args.allow_error_payloads: return 0 - return 1 if _command_failed(payload) else 0 + return int(response.has_errors()) finally: logging.disable(disabled_level) registry_logger.setLevel(registry_level) diff --git a/openhcs/mcp/dev_client_commanding.py b/openhcs/mcp/dev_client_commanding.py index 9f0a9cca9d..e066d5862b 100644 --- a/openhcs/mcp/dev_client_commanding.py +++ b/openhcs/mcp/dev_client_commanding.py @@ -209,6 +209,21 @@ def render_response( del args return json.dumps(payload, indent=2, sort_keys=True) + def render_result(self, response, args: argparse.Namespace) -> str: + """Render a framed production result without requiring a JSON round trip.""" + from openhcs.serialization.json import to_jsonable + + return self.render_response(to_jsonable(response), args) + + +class TypedCompositeCommandSpec(McpDevCommandSpec): + """Composite commands consume the same decoded batch as their wire ingress.""" + + def render_result(self, response, args: argparse.Namespace) -> str: + if args.json: + return super().render_result(response, args) + return self.render_response(response, args) + class CapabilityBackedCommandSpec(McpDevCommandSpec): """Command whose primary MCP tool capability is declared on the command.""" @@ -260,7 +275,9 @@ def parser_aliases(self) -> tuple[str, ...]: def output_renderer_binding(self): output_contract = self.capability.output_contract return McpDevOutputRenderer.for_output_contract( - output_contract if isinstance(output_contract, type) else None + None + if output_contract is None + else require_agent_type_contract(output_contract) ) def configure_reflected_parser(self, parser: argparse.ArgumentParser) -> None: @@ -301,6 +318,12 @@ def render_response( payload, self.renderer_options(args) ) + def render_result(self, response, args: argparse.Namespace) -> str: + binding = self.output_renderer_binding() + if args.json or binding is None: + return super().render_result(response, args) + return binding.render_result(response, self.renderer_options(args)) + class ToolsCommandSpec(McpDevCommandSpec): command = "tools" diff --git a/openhcs/mcp/dev_client_commands/knowledge_pipeline.py b/openhcs/mcp/dev_client_commands/knowledge_pipeline.py index 4c9a92af61..2e07a6e2dc 100644 --- a/openhcs/mcp/dev_client_commands/knowledge_pipeline.py +++ b/openhcs/mcp/dev_client_commands/knowledge_pipeline.py @@ -24,7 +24,10 @@ PipelineSourceRenderRequest, PipelineValidationRequest, ) -from openhcs.mcp.dev_client_commanding import McpDevCommandSpec, SingleToolCommandSpec +from openhcs.mcp.dev_client_commanding import ( + SingleToolCommandSpec, + TypedCompositeCommandSpec, +) from openhcs.mcp.dev_client_core import ( DEFAULT_REGISTRY_DISCOVERY_TIMEOUT_SECONDS, McpDevCliUsageError, @@ -38,9 +41,7 @@ execute_source_session_tool_arguments, execute_source_submit_timeout_seconds, execute_source_submit_tool_arguments, - first_mapping_payload, optional_int, - optional_str, parse_optional_json_object, parse_required_axis_labels, pipeline_source_from_args, @@ -216,13 +217,17 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: ) for connection_field in fields(ExecutionConnectionSpec): add_request_factory_option( - parser, CustomFunctionRegistrationRequest.from_fields, - connection_field.name, f"--{connection_field.name.replace('_', '-')}", + parser, + CustomFunctionRegistrationRequest.from_fields, + connection_field.name, + f"--{connection_field.name.replace('_', '-')}", ) for field_name in ("function_name", "storage_dir"): add_request_factory_option( - parser, CustomFunctionRegistrationRequest.from_fields, - field_name, f"--{field_name.replace('_', '-')}", + parser, + CustomFunctionRegistrationRequest.from_fields, + field_name, + f"--{field_name.replace('_', '-')}", ) parser.add_argument( "--json", @@ -237,10 +242,12 @@ def tool_arguments( source_code = args.source_code if source_code is None: source_code = Path(args.source_file).read_text(encoding="utf-8") - connection = ExecutionConnectionSpec(**{ - connection_field.name: vars(args)[connection_field.name] - for connection_field in fields(ExecutionConnectionSpec) - }) + connection = ExecutionConnectionSpec( + **{ + connection_field.name: vars(args)[connection_field.name] + for connection_field in fields(ExecutionConnectionSpec) + } + ) connection.require_port("Custom function registration") return McpToolArgumentAuthority.from_payload( { @@ -321,7 +328,7 @@ def call_render_args( ) -class DraftPipelineStepCommandSpec(McpDevCommandSpec): +class DraftPipelineStepCommandSpec(TypedCompositeCommandSpec): command = "draft-pipeline-step" help = ( "Create, add one FunctionStep, validate, and render a draft in one MCP session." @@ -380,11 +387,9 @@ async def run_session( timeout_seconds, ) results = [create_result] - create_payload = first_mapping_payload(create_result) - pipeline_id = ( - None if create_payload is None else create_payload.get("pipeline_id") - ) - if isinstance(pipeline_id, str): + create_payload = create_result.first_decoded_payload() + pipeline_id = None if create_payload is None else create_payload.pipeline_id + if pipeline_id is not None: add_arguments: dict[str, JsonValue] = { "pipeline_id": pipeline_id, "function_id": args.function_id, @@ -499,7 +504,7 @@ def tool_arguments( ) -class ExecuteSourceCommandSpec(McpDevCommandSpec): +class ExecuteSourceCommandSpec(TypedCompositeCommandSpec): command = "execute-source" help = "Create and submit a source-backed headless execution session." default_timeout_seconds: ClassVar[float] = 120.0 @@ -574,12 +579,8 @@ async def run_session( timeout_seconds, ) results = [create_result] - create_payload = first_mapping_payload(create_result) - session_id = ( - optional_str(create_payload.get("session_id")) - if create_payload is not None - else None - ) + create_payload = create_result.first_decoded_payload() + session_id = create_payload.session_id if create_payload is not None else None if session_id is not None: results.append( await call_mcp_tool( diff --git a/openhcs/mcp/dev_client_core.py b/openhcs/mcp/dev_client_core.py index 633db24d26..992efe91c5 100644 --- a/openhcs/mcp/dev_client_core.py +++ b/openhcs/mcp/dev_client_core.py @@ -13,8 +13,9 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Mapping, Sequence from contextlib import AsyncExitStack, asynccontextmanager -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields, is_dataclass, replace from enum import Enum +from functools import singledispatch from pathlib import Path from typing import ClassVar, Self, TextIO, TypeVar, cast, get_type_hints @@ -389,13 +390,83 @@ def from_exception( ) +@dataclass(frozen=True, slots=True) +class McpDevPayloadFailure: + """Rejected response contract, retaining the complete external receipt.""" + + receipt: JsonValue + errors: tuple[AgentError, ...] + + +@to_jsonable.register(McpDevPayloadFailure) +def _jsonable_payload_failure(value: McpDevPayloadFailure) -> JsonValue: + return value.receipt + + @dataclass(frozen=True, slots=True) class McpDevToolResult: """JSON-facing result for one MCP tool call.""" tool: str mcp_error: bool - payloads: tuple[JsonValue, ...] + payloads: tuple[object, ...] + + def decoded_for_rendering(self) -> "McpDevToolResult": + """Descend through the capability's existing renderer/output binding.""" + from openhcs.mcp.dev_client_rendering import McpDevOutputRenderer + + try: + capability = get_agent_capability(self.tool) + except KeyError: + return self + bindings = tuple( + McpDevOutputRenderer.for_output_contract(contract) + for contract in capability.output_contract_types + ) + if not bindings or any(binding is None for binding in bindings): + return self + return replace( + self, + payloads=tuple( + self._decode_payload(payload, bindings) for payload in self.payloads + ), + ) + + @staticmethod + def _decode_payload(payload, bindings): + if isinstance(payload, McpDevPayloadFailure): + return payload + rejections: list[AgentError] = [] + for binding in bindings: + try: + return binding.decode_payload(payload) + except (TypeError, ValueError) as error: + rejections.append( + AgentError.from_exception("mcp_payload_invalid", error) + ) + return McpDevPayloadFailure(payload, (*_agent_errors(payload), *rejections)) + + def first_decoded_payload(self): + """A missing or rejected payload is not a successful empty record.""" + if not self.payloads: + return None + payload = self.payloads[0] + return None if isinstance(payload, McpDevPayloadFailure) else payload + + def diagnostic_errors(self) -> tuple[AgentError, ...]: + errors = tuple( + error for payload in self.payloads for error in _agent_errors(payload) + ) + if self.mcp_error and not errors: + return (AgentError(code="mcp_tool_error", message=f"{self.tool} failed."),) + if not self.payloads and not errors: + return ( + AgentError( + code="mcp_payload_missing", + message=f"{self.tool} returned no payload.", + ), + ) + return errors @classmethod def from_payload( @@ -407,20 +478,16 @@ def from_payload( tool=tool_name, mcp_error=result.get("isError") is True, payloads=_content_payloads(result), - ) + ).decoded_for_rendering() def has_errors(self) -> bool: """Return whether the tool or any structured agent payload failed.""" - return self.mcp_error or any( - _contains_agent_error(payload) for payload in self.payloads - ) + return bool(self.diagnostic_errors()) def agent_error_codes(self) -> tuple[str, ...]: """Project structured agent error codes without interpreting their domain.""" - return tuple( - code for payload in self.payloads for code in _agent_error_codes(payload) - ) + return tuple(error.code for error in self.diagnostic_errors()) def has_only_agent_error_code(self, code: str) -> bool: """Return whether every structured failure carries one declared code.""" @@ -645,6 +712,9 @@ class McpDevResponse(ABC): server: McpDevServerIdentity errors: tuple[McpDevTransportFailure, ...] = () + def has_errors(self) -> bool: + return bool(self.errors) + @classmethod def from_transport_failure( cls, @@ -672,6 +742,39 @@ class McpDevToolBatchResponse(McpDevResponse): results: tuple[McpDevToolResult, ...] = () + def has_errors(self) -> bool: + return super(McpDevToolBatchResponse, self).has_errors() or any( + result.has_errors() for result in self.results + ) + + @classmethod + def for_rendering( + cls, + response: "McpDevToolBatchResponse | JsonObject", + ) -> "McpDevToolBatchResponse": + """Decode the external framing and selected DTOs once at view ingress.""" + framed = ( + response + if isinstance(response, cls) + else dataclass_from_mapping(cls, response) + ) + return replace( + framed, + results=tuple(result.decoded_for_rendering() for result in framed.results), + ) + + def payload_for(self, capability: AgentCapabilitySpec): + result = next( + (result for result in self.results if result.tool == capability.name), None + ) + return None if result is None else result.first_decoded_payload() + + def diagnostic_errors(self) -> tuple[AgentError | McpDevTransportFailure, ...]: + return ( + *self.errors, + *(error for result in self.results for error in result.diagnostic_errors()), + ) + @classmethod def from_results( cls, @@ -1039,37 +1142,61 @@ def require_json_object_payload(value: JsonValue) -> JsonObject: return cast(JsonObject, value) -def _contains_agent_error(value: JsonValue) -> bool: - if isinstance(value, Mapping): - errors = AgentResultEnvelope.error_items_from_serialized_mapping(value) - if errors: - return True - return any(_contains_agent_error(child) for child in value.values()) - if isinstance(value, list): - return any(_contains_agent_error(child) for child in value) - return False +@singledispatch +def _agent_errors(value: object) -> tuple[AgentError, ...]: + """One diagnostic descent mechanism over actual declared and JSON values.""" + if is_dataclass(value) and not isinstance(value, type): + return tuple( + error + for declaration in fields(value) + for error in _agent_errors(getattr(value, declaration.name)) + ) + return () -def _agent_error_codes(value: JsonValue) -> tuple[str, ...]: - """Recursively project declared error codes from one JSON-facing payload.""" - - if isinstance(value, Mapping): - projected: list[str] = [] - errors = AgentResultEnvelope.error_items_from_serialized_mapping(value) - if errors is not None: - for error in errors: - if not isinstance(error, Mapping): - continue - code = AgentError.code_from_serialized_mapping(error) - if code is not None: - projected.append(code) - for child in value.values(): - if child is not errors: - projected.extend(_agent_error_codes(child)) - return tuple(projected) - if isinstance(value, list): - return tuple(code for child in value for code in _agent_error_codes(child)) - return () +@_agent_errors.register(AgentError) +def _declared_agent_error(value: AgentError) -> tuple[AgentError, ...]: + return (value,) + + +@_agent_errors.register(McpDevPayloadFailure) +def _failed_payload_errors(value: McpDevPayloadFailure) -> tuple[AgentError, ...]: + return value.errors + + +@_agent_errors.register(Mapping) +def _boundary_agent_errors(value: Mapping) -> tuple[AgentError, ...]: + """JSON extension bags and unmigrated wire payloads remain real mappings.""" + errors = AgentResultEnvelope.error_items_from_serialized_mapping(value) + projected: list[AgentError] = [] + for error in errors or (): + try: + projected.append(dataclass_from_mapping(AgentError, error)) + except (TypeError, ValueError) as exception: + projected.append( + AgentError.from_exception("mcp_diagnostic_invalid", exception) + ) + projected.extend( + error + for child in value.values() + if child is not errors + for error in _agent_errors(child) + ) + return tuple(projected) + + +@_agent_errors.register(list) +@_agent_errors.register(tuple) +def _sequence_agent_errors(value) -> tuple[AgentError, ...]: + return tuple(error for child in value for error in _agent_errors(child)) + + +def _contains_agent_error(value: object) -> bool: + return bool(_agent_errors(value)) + + +def _agent_error_codes(value: object) -> tuple[str, ...]: + return tuple(error.code for error in _agent_errors(value)) def _command_failed(payload: JsonObject) -> bool: diff --git a/openhcs/mcp/dev_client_renderers/knowledge.py b/openhcs/mcp/dev_client_renderers/knowledge.py index 1d5cbb9947..aa47ecbefe 100644 --- a/openhcs/mcp/dev_client_renderers/knowledge.py +++ b/openhcs/mcp/dev_client_renderers/knowledge.py @@ -26,11 +26,11 @@ from openhcs.mcp.dev_client_rendering import ( AuthoringContextRenderOptions, CatalogRenderOptions, + CodeDocumentRenderOptions, McpDevOutputRenderer, McpDevPayloadProjection, ) from openhcs.mcp.dev_client_renderers.object_state import ObjectStateScopeRenderer -from openhcs.mcp.dev_client_renderers.ui_bridge import CodeDocumentRenderer from openhcs.mcp.dev_client_renderers.viewer import ( RuntimeServerRenderer, ViewerValidationRenderer, @@ -286,7 +286,7 @@ def render( bounded_limit = max(limit, 0) visible_topics = topics[:bounded_limit] lines = [ - "Architecture topics: " f"matched={len(topics)} shown={len(visible_topics)}" + f"Architecture topics: matched={len(topics)} shown={len(visible_topics)}" ] if contains: lines.append(f"Filter: contains={contains}") @@ -660,6 +660,8 @@ def render(cls, response: JsonObject, *, max_chars: int = 2_000) -> str: if isinstance(content, str): lines.append("Content:") lines.append( - CodeDocumentRenderer._source_text(content, max_source_chars=max_chars) + CodeDocumentRenderOptions(max_source_chars=max_chars).source_text( + content + ) ) return "\n".join(lines) diff --git a/openhcs/mcp/dev_client_renderers/pipeline.py b/openhcs/mcp/dev_client_renderers/pipeline.py index 8c0933352e..af26b234ed 100644 --- a/openhcs/mcp/dev_client_renderers/pipeline.py +++ b/openhcs/mcp/dev_client_renderers/pipeline.py @@ -1,174 +1,199 @@ -"""Pipeline authoring and execution renderers for the MCP dev client.""" +"""Typed pipeline authoring, planning and execution presentation owners.""" from __future__ import annotations -import json -from collections.abc import Mapping from inspect import getdoc from openhcs.agent.capabilities import agent_capabilities -from openhcs.agent.dto.common import JsonObject, JsonValue -from openhcs.agent.dto.execution import ArtifactPlanInspection, SourceWorkspaceSummary +from openhcs.agent.dto.common import AgentError, AgentWarning, RenderedSource +from openhcs.agent.dto.execution import ( + ArtifactInputPlanSummary, + ArtifactMaterializationPlanSummary, + ArtifactPlanInspection, + CompiledStepPlanSummary, + ExecutionJobRef, + ExecutionJobIdentity, + ExecutionJobStatus, + OrchestratorSessionRef, + SourceWorkspaceSummary, +) +from openhcs.agent.dto.pipeline import ( + FunctionStepSpec, + PipelineRef, + PipelineSpec, + PipelineValidationResult, +) +from openhcs.mcp.dev_client_core import McpDevToolBatchResponse from openhcs.mcp.dev_client_rendering import ( - McpDevOutputRenderer, + CodeDocumentRenderOptions, + McpDevOutputRenderOptions, McpDevPayloadProjection, + McpDevTypedOutputRenderer, + McpDiagnosticRenderer, ) -from openhcs.mcp.dev_client_core import optional_bool, optional_int -from openhcs.mcp.dev_client_renderers.object_state import ObjectStateScopeRenderer -from openhcs.mcp.dev_client_renderers.ui_bridge import CodeDocumentRenderer -from openhcs.mcp.dev_client_renderers.viewer import ViewerValidationRenderer -class PipelineDraftStepRenderer: - """Compact renderer for one-step pipeline draft smoke checks.""" +def _sequence_text(values) -> str: + return ",".join(str(value) for value in values) + + +def _mapping_text(values) -> str: + return ", ".join( + f"{key}={McpDevPayloadProjection.text(value)}" for key, value in values.items() + ) + + +def _append_messages( + lines: list[str], + errors: tuple[AgentError, ...], + warnings: tuple[AgentWarning, ...], + label: str = "", +) -> None: + prefix = f"{label} " if label else "" + if errors: + lines.append(f"{prefix}errors:") + lines.extend(McpDiagnosticRenderer.typed_error_lines(errors)) + if warnings: + lines.append(f"{prefix}warnings:") + lines.extend(McpDiagnosticRenderer.typed_error_lines(warnings)) + + +class PipelineReferenceRenderer(McpDevTypedOutputRenderer): + output_contract = PipelineRef @classmethod - def render(cls, response: JsonObject, *, max_source_chars: int = 2_000) -> str: - create_payload = cls._payload_for_tool( - response, agent_capabilities.create_pipeline.name - ) - add_payload = cls._payload_for_tool( - response, agent_capabilities.add_function_step.name - ) - validate_payload = cls._payload_for_tool( - response, agent_capabilities.validate_pipeline.name - ) - render_payload = cls._payload_for_tool( - response, agent_capabilities.render_pipeline_source.name - ) - pipeline_id = ( - create_payload.get("pipeline_id") if create_payload is not None else None - ) - pipeline_steps = ( - McpDevPayloadProjection.sequence_of_mappings(add_payload.get("steps")) - if add_payload is not None - else () - ) - lines = [ + def render_payload( + cls, payload: PipelineRef, options: McpDevOutputRenderOptions + ) -> str: + return f"Pipeline: id={payload.pipeline_id} uri={payload.uri}" + + +class PipelineSpecRenderer(McpDevTypedOutputRenderer): + output_contract = PipelineSpec + + @classmethod + def render_payload( + cls, payload: PipelineSpec, options: McpDevOutputRenderOptions + ) -> str: + return "\n".join( ( - "Pipeline draft: " - f"id={McpDevPayloadProjection.text(pipeline_id)} " - f"valid={cls._valid_text(validate_payload)} " - f"steps={len(pipeline_steps)}" + f"Pipeline: id={payload.pipeline_id} steps={len(payload.steps)}", + *cls.step_lines(payload.steps), ) + ) + + @staticmethod + def step_lines(steps: tuple[FunctionStepSpec, ...]) -> list[str]: + return [ + f"- {step.step_id}: name={McpDevPayloadProjection.quoted_text(step.name)} " + f"enabled={step.enabled} functions={','.join(function.function_id for function in step.functions) or ''}" + for step in steps ] - if create_payload is not None: - lines.append( - f"Ref: uri={McpDevPayloadProjection.text(create_payload.get('uri'))}" - ) - cls._append_payload_messages(lines, create_payload, "Create") - cls._append_payload_messages(lines, add_payload, "Add step") - cls._append_payload_messages(lines, validate_payload, "Validate") - cls._append_repair_hints(lines, validate_payload, pipeline_steps) - if pipeline_steps: - lines.append("Steps:") - lines.extend(cls._step_lines(pipeline_steps)) - if render_payload is not None: - source = render_payload.get("source") - if isinstance(source, str): - lines.append( - "Source: " - f"title={McpDevPayloadProjection.quoted_text(render_payload.get('title'))} " - f"bytes={len(source)}" - ) - lines.append( - CodeDocumentRenderer._source_text( - source, - max_source_chars=max_source_chars, - ) - ) - cls._append_payload_messages(lines, render_payload, "Render") + + +class PipelineValidationRenderer(McpDevTypedOutputRenderer): + output_contract = PipelineValidationResult + + @classmethod + def render_payload( + cls, payload: PipelineValidationResult, options: McpDevOutputRenderOptions + ) -> str: + lines = [ + f"Pipeline validation: id={payload.pipeline_ref.pipeline_id} valid={payload.valid}" + ] + _append_messages(lines, (), payload.warnings) return "\n".join(lines) - @staticmethod - def _payload_for_tool( - response: JsonObject, - tool_name: str, - ) -> Mapping[str, JsonValue] | None: - return McpDevPayloadProjection.tool_payload(response, tool_name) - @staticmethod - def _valid_text(payload: Mapping[str, JsonValue] | None) -> str: - if payload is None: - return "" - return McpDevPayloadProjection.text(payload.get("valid")) +class PipelineSourceRenderer(McpDevTypedOutputRenderer): + output_contract = RenderedSource + render_options_type = CodeDocumentRenderOptions @classmethod - def _step_lines(cls, steps: tuple[Mapping[str, JsonValue], ...]) -> list[str]: - lines: list[str] = [] - for step in steps: - functions = McpDevPayloadProjection.sequence_of_mappings( - step.get("functions") - ) - function_ids = tuple( - McpDevPayloadProjection.text(function.get("function_id")) - for function in functions - ) + def render_payload( + cls, payload: RenderedSource, options: CodeDocumentRenderOptions + ) -> str: + lines = [ + f"Source: title={McpDevPayloadProjection.quoted_text(payload.title)} bytes={len(payload.source)}" + ] + if options.include_source: + lines.append(options.source_text(payload.source)) + return "\n".join(lines) + + +class PipelineDraftStepRenderer: + """Composite presentation of the same decoded production authoring results.""" + + @classmethod + def render(cls, response, *, max_source_chars: int = 2_000) -> str: + decoded = McpDevToolBatchResponse.for_rendering(response) + created: PipelineRef | None = decoded.payload_for( + agent_capabilities.create_pipeline + ) + added: PipelineSpec | None = decoded.payload_for( + agent_capabilities.add_function_step + ) + validated: PipelineValidationResult | None = decoded.payload_for( + agent_capabilities.validate_pipeline + ) + source: RenderedSource | None = decoded.payload_for( + agent_capabilities.render_pipeline_source + ) + steps = () if added is None else added.steps + lines = [ + f"Pipeline draft: id={McpDevPayloadProjection.text(None if created is None else created.pipeline_id)} " + f"valid={'' if validated is None else validated.valid} steps={len(steps)}" + ] + if created is not None: + lines.append(f"Ref: uri={created.uri}") + if validated is not None and validated.errors: + lines.append("Validate errors:") + lines.extend( + McpDiagnosticRenderer.typed_error_lines(decoded.diagnostic_errors()) + ) + if validated is not None: + _append_messages(lines, (), validated.warnings, "Validate") + cls._append_repair_hints(lines, validated.errors, steps) + if steps: + lines.append("Steps:") + lines.extend(PipelineSpecRenderer.step_lines(steps)) + if source is not None: lines.append( - "- " - f"{McpDevPayloadProjection.text(step.get('step_id'))}: " - f"name={McpDevPayloadProjection.quoted_text(step.get('name'))} " - f"enabled={McpDevPayloadProjection.text(step.get('enabled'))} " - f"functions={','.join(function_ids) if function_ids else ''}" + PipelineSourceRenderer.render_payload( + source, CodeDocumentRenderOptions(max_source_chars=max_source_chars) + ) ) - return lines - - @staticmethod - def _append_payload_messages( - lines: list[str], - payload: Mapping[str, JsonValue] | None, - label: str, - ) -> None: - if payload is None: - return - errors = McpDevPayloadProjection.sequence_of_mappings(payload.get("errors")) - if errors: - lines.append(f"{label} errors:") - lines.extend(ViewerValidationRenderer._error_lines(errors)) - warnings = McpDevPayloadProjection.sequence_of_mappings(payload.get("warnings")) - if warnings: - lines.append(f"{label} warnings:") - lines.extend(ViewerValidationRenderer._error_lines(warnings)) + return "\n".join(lines) @classmethod def _append_repair_hints( cls, lines: list[str], - validate_payload: Mapping[str, JsonValue] | None, - steps: tuple[Mapping[str, JsonValue], ...], + errors: tuple[AgentError, ...], + steps: tuple[FunctionStepSpec, ...], ) -> None: - if validate_payload is None or not steps: - return - errors = McpDevPayloadProjection.sequence_of_mappings( - validate_payload.get("errors") - ) missing_kwargs = cls._missing_function_kwargs(errors) - if not missing_kwargs: - return - function_id = cls._first_function_id(steps) - if function_id is None: + functions = tuple(function for step in steps for function in step.functions) + if not missing_kwargs or not functions: return + function_id = functions[0].function_id kwargs_shape = ", ".join(f'"{name}": ' for name in missing_kwargs) lines.append(f"Next: function {function_id}") lines.append( - "Retry shape: " - f"draft-pipeline-step {function_id} --kwargs '{{{kwargs_shape}}}'" + f"Retry shape: draft-pipeline-step {function_id} --kwargs '{{{kwargs_shape}}}'" ) - @staticmethod + @classmethod def _missing_function_kwargs( - errors: tuple[Mapping[str, JsonValue], ...], + cls, errors: tuple[AgentError, ...] ) -> tuple[str, ...]: for error in errors: - if error.get("code") != "missing_function_kwargs": - continue - for key in ("hint", "message"): - value = error.get(key) - if not isinstance(value, str): - continue - names = PipelineDraftStepRenderer._parse_missing_kwargs(value) - if names: - return names + if error.code == "missing_function_kwargs": + for text in (error.hint, error.message): + if text is not None: + names = cls._parse_missing_kwargs(text) + if names: + return names return () @staticmethod @@ -181,355 +206,220 @@ def _parse_missing_kwargs(text: str) -> tuple[str, ...]: else: return () tail = tail.strip().rstrip(".") - names = tuple( + return tuple( name.strip().strip("`'") for name in tail.split(",") if name.strip() ) - return names - @staticmethod - def _first_function_id( - steps: tuple[Mapping[str, JsonValue], ...], - ) -> str | None: - for step in steps: - functions = McpDevPayloadProjection.sequence_of_mappings( - step.get("functions") - ) - for function in functions: - function_id = function.get("function_id") - if isinstance(function_id, str) and function_id: - return function_id - return None - - -class PipelineArtifactPlanRenderer(McpDevOutputRenderer): - """Compact renderer for pycodified pipeline artifact-plan inspection.""" +class PipelineArtifactPlanRenderer(McpDevTypedOutputRenderer): output_contract = ArtifactPlanInspection @classmethod - def render(cls, response: JsonObject) -> str: - payload = McpDevPayloadProjection.first_tool_payload(response) - if payload is None: - return json.dumps(response, indent=2, sort_keys=True) + def render_payload( + cls, payload: ArtifactPlanInspection, options: McpDevOutputRenderOptions + ) -> str: lines = [ - ( - "Artifact plan: " - f"plate={McpDevPayloadProjection.text(payload.get('plate_path'))} " - f"axes={McpDevPayloadProjection.text(payload.get('axis_count'))} " - f"steps={McpDevPayloadProjection.text(payload.get('step_count'))} " - f"progress_events={McpDevPayloadProjection.text(payload.get('progress_event_count'))}" - ) + f"Artifact plan: plate={payload.plate_path} axes={payload.axis_count} steps={payload.step_count} progress_events={payload.progress_event_count}" ] - ObjectStateScopeRenderer._append_messages(lines, payload) - axes = payload.get("axes") - if axes: - lines.append(f"Axes: {ViewerValidationRenderer._sequence_text(axes)}") - axis_filter = payload.get("axis_filter") - if axis_filter: + _append_messages(lines, (), payload.warnings) + if payload.axes: + lines.append(f"Axes: {_sequence_text(payload.axes)}") + if payload.axis_filter: + lines.append(f"Axis filter: {_sequence_text(payload.axis_filter)}") + lines.extend(cls._source_workspace_lines(payload.source_workspace)) + if payload.worker_assignments: lines.append( - f"Axis filter: {ViewerValidationRenderer._sequence_text(axis_filter)}" + "Workers: " + + ", ".join( + f"{worker}=[{_sequence_text(axes)}]" + for worker, axes in payload.worker_assignments.items() + ) ) - source_workspace = McpDevPayloadProjection.nested_mapping( - payload, - "source_workspace", - ) - if source_workspace: - lines.extend(cls._source_workspace_lines(source_workspace)) - worker_assignments = McpDevPayloadProjection.nested_mapping( - payload, - "worker_assignments", - ) - if worker_assignments: - lines.append(f"Workers: {cls._worker_text(worker_assignments)}") - steps = McpDevPayloadProjection.sequence_of_mappings(payload.get("steps")) - if steps: + if payload.steps: lines.append("Steps:") - lines.extend(cls._step_lines(steps)) + lines.extend(cls._step_lines(payload.steps)) return "\n".join(lines) - @classmethod - def _source_workspace_lines( - cls, - source_workspace: Mapping[str, JsonValue], - ) -> list[str]: + @staticmethod + def _source_workspace_lines(source_workspace: SourceWorkspaceSummary) -> list[str]: lines = [ - ( - "Source workspace (source-bound files): " - f"files={McpDevPayloadProjection.text(source_workspace.get('file_count'))} " - f"truncated={McpDevPayloadProjection.text(source_workspace.get('truncated_file_count'))}" - ) + f"Source workspace (source-bound files): files={source_workspace.file_count} truncated={source_workspace.truncated_file_count}" ] - axis_file_counts = McpDevPayloadProjection.nested_mapping( - source_workspace, - "axis_file_counts", - ) - if axis_file_counts: - lines.append(f" axis files: {cls._mapping_text(axis_file_counts)}") - if optional_int(source_workspace.get("file_count")) == 0: - lines.append(f" note: {getdoc(SourceWorkspaceSummary)}") - files = McpDevPayloadProjection.sequence_of_mappings( - source_workspace.get("files") - ) - for file_record in files[:5]: - virtual_path = McpDevPayloadProjection.text(file_record.get("virtual_path")) - source_path = McpDevPayloadProjection.text(file_record.get("source_path")) - metadata = McpDevPayloadProjection.nested_mapping( - file_record, - "source_metadata", + if source_workspace.axis_file_counts: + lines.append( + f" axis files: {_mapping_text(source_workspace.axis_file_counts)}" ) - line = f" - {virtual_path}" - if source_path and source_path != "" and source_path != virtual_path: - line += f" -> {source_path}" - if metadata: - line += f" components={cls._mapping_text(metadata)}" + if source_workspace.file_count == 0: + lines.append(f" note: {getdoc(SourceWorkspaceSummary)}") + for record in source_workspace.files[:5]: + line = f" - {record.virtual_path}" + if record.source_path and record.source_path != record.virtual_path: + line += f" -> {record.source_path}" + if record.source_metadata: + line += f" components={_mapping_text(record.source_metadata)}" lines.append(line) - if len(files) > 5: + if len(source_workspace.files) > 5: lines.append(" - ...") return lines @classmethod - def _step_lines( - cls, - steps: tuple[Mapping[str, JsonValue], ...], - ) -> list[str]: + def _step_lines(cls, steps: tuple[CompiledStepPlanSummary, ...]) -> list[str]: lines: list[str] = [] for step in steps: lines.append( - "- " - f"{McpDevPayloadProjection.text(step.get('step_index'))}: " - f"{McpDevPayloadProjection.text(step.get('step_name'))} " - f"axis={McpDevPayloadProjection.text(step.get('axis_id'))} " - f"groups={ViewerValidationRenderer._sequence_text(step.get('execution_groups'))}" - ) - axis_persistence = optional_bool( - step.get("main_flow_axis_persistence_enabled") + f"- {step.step_index}: {step.step_name} axis={step.axis_id} groups={_sequence_text(step.execution_groups)}" ) - if axis_persistence is not None: + if step.main_flow_axis_persistence_enabled is not None: lines.append( " main-flow axis persistence: " - f"{'enabled' if axis_persistence else 'runtime-only'}" + + ( + "enabled" + if step.main_flow_axis_persistence_enabled + else "runtime-only" + ) ) - main_flow_materialization = McpDevPayloadProjection.nested_mapping( - step, - "main_flow_materialization", - ) - if main_flow_materialization: + checkpoint = step.main_flow_materialization + if checkpoint is not None: lines.append( - " main-flow checkpoint: " - f"backend={McpDevPayloadProjection.text(main_flow_materialization.get('backend'))} " - f"output_dir={McpDevPayloadProjection.text(main_flow_materialization.get('output_dir'))} " - f"sub_dir={McpDevPayloadProjection.text(main_flow_materialization.get('sub_dir'))}" + f" main-flow checkpoint: backend={checkpoint.backend} output_dir={checkpoint.output_dir} sub_dir={checkpoint.sub_dir}" ) - viewer_streaming = McpDevPayloadProjection.sequence_of_mappings( - step.get("viewer_streaming") - ) - for viewer in viewer_streaming: - effective_config = McpDevPayloadProjection.nested_mapping( - viewer, - "effective_config", - ) - line = ( - " viewer stream: " - f"viewer={McpDevPayloadProjection.text(viewer.get('viewer_type'))} " - f"config={McpDevPayloadProjection.text(viewer.get('config_key'))} " - f"backend={McpDevPayloadProjection.text(viewer.get('backend'))}" - ) - if effective_config: - line += f" effective={cls._mapping_text(effective_config)}" + for viewer in step.viewer_streaming: + line = f" viewer stream: viewer={viewer.viewer_type.value} config={viewer.config_key} backend={viewer.backend}" + if viewer.effective_config: + line += f" effective={_mapping_text(viewer.effective_config)}" lines.append(line) - artifact_inputs = McpDevPayloadProjection.sequence_of_mappings( - step.get("artifact_inputs") - ) - for artifact in artifact_inputs: + for artifact in step.artifact_inputs: lines.append(cls._artifact_input_line(artifact)) - artifacts = McpDevPayloadProjection.sequence_of_mappings( - step.get("artifact_outputs") - ) - for artifact in artifacts: + for artifact in step.artifact_outputs: lines.append( - " artifact " - f"{McpDevPayloadProjection.text(artifact.get('name'))}: " - f"kind={McpDevPayloadProjection.text(artifact.get('kind'))} " - f"path={McpDevPayloadProjection.text(artifact.get('path'))} " - f"groups={ViewerValidationRenderer._sequence_text(artifact.get('group_keys'))}" + f" artifact {artifact.name}: kind={artifact.kind} path={artifact.path} groups={_sequence_text(artifact.group_keys)}" ) - materialization = McpDevPayloadProjection.nested_mapping( - artifact, - "materialization", + if artifact.materialization is not None: + lines.extend(cls._materialization_lines(artifact.materialization)) + if step.truncated_artifact_input_count > 0: + lines.append( + f" artifact input ... truncated={step.truncated_artifact_input_count}" + ) + if step.truncated_artifact_output_count > 0: + lines.append( + f" artifact ... truncated={step.truncated_artifact_output_count}" ) - if materialization: - lines.extend(cls._materialization_lines(materialization)) - truncated_input_count = step.get("truncated_artifact_input_count") - if isinstance(truncated_input_count, int) and truncated_input_count > 0: - lines.append(f" artifact input ... truncated={truncated_input_count}") - truncated_count = step.get("truncated_artifact_output_count") - if isinstance(truncated_count, int) and truncated_count > 0: - lines.append(f" artifact ... truncated={truncated_count}") return lines @staticmethod - def _artifact_input_line(artifact: Mapping[str, JsonValue]) -> str: + def _artifact_input_line(artifact: ArtifactInputPlanSummary) -> str: source_parts: list[str] = [] - source_step_id = artifact.get("source_step_id") - if source_step_id is not None: - source_parts.append( - f"source_step={McpDevPayloadProjection.text(source_step_id)}" - ) - source_step_scope_id = artifact.get("source_step_scope_id") - if source_step_scope_id is not None: - source_parts.append( - f"source_scope={McpDevPayloadProjection.text(source_step_scope_id)}" - ) + if artifact.source_step_id is not None: + source_parts.append(f"source_step={artifact.source_step_id}") + if artifact.source_step_scope_id is not None: + source_parts.append(f"source_scope={artifact.source_step_scope_id}") suffix = f" {' '.join(source_parts)}" if source_parts else "" - return ( - " artifact input " - f"{McpDevPayloadProjection.text(artifact.get('name'))}: " - f"kind={McpDevPayloadProjection.text(artifact.get('kind'))} " - f"path={McpDevPayloadProjection.text(artifact.get('path'))} " - f"groups={ViewerValidationRenderer._sequence_text(artifact.get('group_keys'))}" - f"{suffix}" - ) + return f" artifact input {artifact.name}: kind={artifact.kind} path={artifact.path} groups={_sequence_text(artifact.group_keys)}{suffix}" - @classmethod + @staticmethod def _materialization_lines( - cls, - materialization: Mapping[str, JsonValue], + materialization: ArtifactMaterializationPlanSummary, ) -> list[str]: - status_parts: list[str] = [] - if optional_bool(materialization.get("disabled")) is True: - status_parts.append("disabled") - elif optional_bool(materialization.get("runtime_resolved")) is True: - status_parts.append("runtime-resolved") - else: - status_parts.append("explicit") - status_parts.append( - "persistent=" - f"{McpDevPayloadProjection.text(materialization.get('persistent_enabled'))}" + mode = ( + "disabled" + if materialization.disabled + else "runtime-resolved" + if materialization.runtime_resolved + else "explicit" ) - backend = materialization.get("persistent_backend") - if backend is not None: - status_parts.append(f"backend={McpDevPayloadProjection.text(backend)}") - analysis_output_dir = materialization.get("analysis_output_dir") - if analysis_output_dir is not None: - status_parts.append( - f"analysis_dir={McpDevPayloadProjection.text(analysis_output_dir)}" + parts = [mode, f"persistent={materialization.persistent_enabled}"] + if materialization.persistent_backend is not None: + parts.append(f"backend={materialization.persistent_backend}") + if materialization.analysis_output_dir is not None: + parts.append(f"analysis_dir={materialization.analysis_output_dir}") + if materialization.filename_uses_source_identity: + parts.append("source-identity-filenames") + if materialization.runtime_metadata_can_refine_paths: + parts.append("runtime-metadata-filenames") + lines = [f" materialization: {' '.join(parts)}"] + for path in materialization.paths[:3]: + lines.append( + f" candidates group={McpDevPayloadProjection.text(path.group_key)}: {', '.join(path.candidate_paths)}" ) - if optional_bool(materialization.get("filename_uses_source_identity")) is True: - status_parts.append("source-identity-filenames") - if ( - optional_bool(materialization.get("runtime_metadata_can_refine_paths")) - is True - ): - status_parts.append("runtime-metadata-filenames") - - lines = [f" materialization: {' '.join(status_parts)}"] - paths = McpDevPayloadProjection.sequence_of_mappings( - materialization.get("paths") - ) - for path in paths[:3]: - group_key = McpDevPayloadProjection.text(path.get("group_key")) - candidates = cls._candidate_path_text(path.get("candidate_paths")) - lines.append(" candidates " f"group={group_key}: {candidates}") - if len(paths) > 3: - lines.append(f" candidates ... truncated={len(paths) - 3}") - note = materialization.get("note") - if note is not None: - lines.append(f" note: {McpDevPayloadProjection.text(note)}") + if len(materialization.paths) > 3: + lines.append( + f" candidates ... truncated={len(materialization.paths) - 3}" + ) + if materialization.note is not None: + lines.append(f" note: {materialization.note}") return lines - @staticmethod - def _candidate_path_text(value: JsonValue) -> str: - if isinstance(value, list | tuple): - return ", ".join(McpDevPayloadProjection.text(item) for item in value) - return McpDevPayloadProjection.text(value) - @staticmethod - def _mapping_text(mapping: Mapping[str, JsonValue]) -> str: - return ", ".join( - f"{key}={McpDevPayloadProjection.text(value)}" - for key, value in mapping.items() - ) +class OrchestratorSessionReferenceRenderer(McpDevTypedOutputRenderer): + output_contract = OrchestratorSessionRef @classmethod - def _worker_text(cls, mapping: Mapping[str, JsonValue]) -> str: - return ", ".join( - f"{worker}=[{ViewerValidationRenderer._sequence_text(axes)}]" - for worker, axes in mapping.items() - ) + def render_payload( + cls, payload: OrchestratorSessionRef, options: McpDevOutputRenderOptions + ) -> str: + return f"Session: id={payload.session_id} uri={payload.uri}" -class ExecuteSourceRenderer: - """Compact renderer for source-backed headless execution command results.""" +class ExecutionJobRenderer(McpDevTypedOutputRenderer): + """Presentation shared by the actual job-identity family, not sibling DTOs.""" + + @staticmethod + def identity_line(payload: ExecutionJobIdentity, status: str) -> str: + return f"Job: id={payload.job_id} kind={payload.kind} status={status} server_execution={McpDevPayloadProjection.text(payload.server_execution_id)}" - SESSION_TOOL = ( - agent_capabilities.create_orchestrator_session_from_pipeline_source.name - ) - SUBMIT_TOOL = agent_capabilities.submit_pipeline_execution.name + +class ExecutionJobReferenceRenderer(ExecutionJobRenderer): + output_contract = ExecutionJobRef @classmethod - def render(cls, response: JsonObject) -> str: - session_payload = McpDevPayloadProjection.tool_payload( - response, - cls.SESSION_TOOL, - ) - submit_payload = McpDevPayloadProjection.tool_payload( - response, - cls.SUBMIT_TOOL, - ) - if session_payload is None and submit_payload is None: - return json.dumps(response, indent=2, sort_keys=True) + def render_payload( + cls, payload: ExecutionJobRef, options: McpDevOutputRenderOptions + ) -> str: + return cls.identity_line(payload, payload.status) - lines = ["Headless source execution:"] - if session_payload is not None: - lines.append( - "Session: " - f"id={McpDevPayloadProjection.text(session_payload.get('session_id'))} " - f"uri={McpDevPayloadProjection.text(session_payload.get('uri'))}" - ) - ObjectStateScopeRenderer._append_messages(lines, session_payload) - else: - lines.append("Session: ") - if submit_payload is not None: +class ExecutionJobStatusRenderer(ExecutionJobRenderer): + output_contract = ExecutionJobStatus + + @classmethod + def render_payload( + cls, payload: ExecutionJobStatus, options: McpDevOutputRenderOptions + ) -> str: + lines = [cls.identity_line(payload, payload.status)] + if payload.response: lines.append( - "Job: " - f"id={McpDevPayloadProjection.text(submit_payload.get('job_id'))} " - f"kind={McpDevPayloadProjection.text(submit_payload.get('kind'))} " - f"status={McpDevPayloadProjection.text(submit_payload.get('status'))} " - "server_execution=" - f"{McpDevPayloadProjection.text(submit_payload.get('server_execution_id'))}" + "Response: " + + " ".join(f"{key}={value}" for key, value in payload.response.items()) ) - response_payload = McpDevPayloadProjection.nested_mapping( - submit_payload, - "response", - ) - if response_payload: - lines.append( - "Response: " + cls._response_summary_text(response_payload) - ) - ObjectStateScopeRenderer._append_messages(lines, submit_payload) - else: - lines.append("Job: ") + if payload.progress is not None: + lines.append(f"Progress: {payload.progress}") + _append_messages(lines, (), payload.warnings) return "\n".join(lines) - @staticmethod - def _response_summary_text(response_payload: Mapping[str, JsonValue]) -> str: - summary_keys = ( - "status", - "state", - "message", - "execution_id", - "server_execution_id", - "completed", - "success", + +class ExecuteSourceRenderer: + @classmethod + def render(cls, response) -> str: + decoded = McpDevToolBatchResponse.for_rendering(response) + session: OrchestratorSessionRef | None = decoded.payload_for( + agent_capabilities.create_orchestrator_session_from_pipeline_source ) - parts = [ - f"{key}={McpDevPayloadProjection.text(response_payload.get(key))}" - for key in summary_keys - if key in response_payload - ] - if parts: - return " ".join(parts) - return ( - f"keys={ViewerValidationRenderer._sequence_text(sorted(response_payload))}" + job: ExecutionJobRef | ExecutionJobStatus | None = decoded.payload_for( + agent_capabilities.submit_pipeline_execution + ) + options = McpDevOutputRenderOptions() + lines = ["Headless source execution:"] + lines.append( + "Session: " + if session is None + else OrchestratorSessionReferenceRenderer.render_payload(session, options) ) + lines.append( + "Job: " + if job is None + else McpDevTypedOutputRenderer.render_payload_value(job, options) + ) + lines.extend( + McpDiagnosticRenderer.typed_error_lines(decoded.diagnostic_errors()) + ) + return "\n".join(lines) diff --git a/openhcs/mcp/dev_client_renderers/ui_bridge.py b/openhcs/mcp/dev_client_renderers/ui_bridge.py index a6ea7d8c98..6b13af6c53 100644 --- a/openhcs/mcp/dev_client_renderers/ui_bridge.py +++ b/openhcs/mcp/dev_client_renderers/ui_bridge.py @@ -192,7 +192,7 @@ def render(cls, response: JsonObject) -> str: results = McpDevPayloadProjection.sequence_of_mappings(response.get("results")) mcp_errors = sum(1 for result in results if result.get("mcp_error") is True) - lines = ["UI smoke: " f"results={len(results)} mcp_errors={mcp_errors}"] + lines = [f"UI smoke: results={len(results)} mcp_errors={mcp_errors}"] lines.append(cls._health_line(response)) lines.extend( UiBridgeStatusRenderer.render( @@ -782,7 +782,7 @@ def _action_lines( suffix = "" if disabled: suffix = ( - " disabled=" f"{McpDevPayloadProjection.text(disabled.get('code'))}" + f" disabled={McpDevPayloadProjection.text(disabled.get('code'))}" ) lines.append( "- " @@ -1250,20 +1250,13 @@ def render( source = payload.get("source") if include_source and isinstance(source, str): lines.append("Source:") - lines.append(cls._source_text(source, max_source_chars=max_source_chars)) + lines.append( + CodeDocumentRenderOptions( + max_source_chars=max_source_chars + ).source_text(source) + ) return "\n".join(lines) - @staticmethod - def _source_text(source: str, *, max_source_chars: int) -> str: - if max_source_chars < 0: - raise ValueError("max_source_chars must be nonnegative.") - if len(source) <= max_source_chars: - return source - return ( - source[:max_source_chars] - + f"\n..." - ) - class CodeDocumentValidationRenderer(McpDevOutputRenderer): """Compact renderer for UI code-document validation results.""" diff --git a/openhcs/mcp/dev_client_rendering.py b/openhcs/mcp/dev_client_rendering.py index c193e50f76..93d053166f 100644 --- a/openhcs/mcp/dev_client_rendering.py +++ b/openhcs/mcp/dev_client_rendering.py @@ -10,13 +10,14 @@ from typing import ClassVar, TypeAlias from metaclass_registry import AutoRegisterMeta +from python_introspect import dataclass_from_mapping from openhcs.agent.capabilities import ( AgentCapabilitySpec, CapabilityWorkflowGroup, get_agent_capability, ) -from openhcs.agent.dto.common import JsonObject, JsonValue +from openhcs.agent.dto.common import AgentError, JsonObject, JsonValue DEFAULT_CODE_DOCUMENT_MAX_CHARS = 2_000 @@ -62,6 +63,17 @@ class CodeDocumentRenderOptions(McpDevOutputRenderOptions): include_source: bool = True max_source_chars: int = DEFAULT_CODE_DOCUMENT_MAX_CHARS + def source_text(self, source: str) -> str: + """One bounded source-text policy shared by all source presentations.""" + if self.max_source_chars < 0: + raise ValueError("max_source_chars must be nonnegative.") + if len(source) <= self.max_source_chars: + return source + return ( + source[: self.max_source_chars] + + f"\n..." + ) + @dataclass(frozen=True, slots=True) class UiActionCatalogRenderOptions(McpDevOutputRenderOptions): @@ -140,14 +152,25 @@ class McpDevOutputRenderer(metaclass=AutoRegisterMeta): render_options_type: ClassVar[type[McpDevOutputRenderOptions]] = ( McpDevOutputRenderOptions ) - __renderer_types__: ClassVar[tuple[type["McpDevOutputRenderer"], ...]] = () - def __init_subclass__(cls, **kwargs: JsonValue) -> None: + def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - McpDevOutputRenderer.__renderer_types__ = ( - *McpDevOutputRenderer.__renderer_types__, - cls, - ) + # AutoRegisterMeta prefers an explicit (including inherited) key. A + # child must register its own declaration, not overwrite its parent's + # key merely because the metaclass assigned that key to the parent. + cls.renderer_key = mcp_dev_output_renderer_key(cls.__name__, cls) + + @classmethod + def declaration_types(cls): + """Derived hierarchy view, visiting a diamond's identity only once.""" + pending = list(cls.__subclasses__()) + seen: set[type] = set() + while pending: + member = pending.pop(0) + if member not in seen: + seen.add(member) + yield member + pending.extend(member.__subclasses__()) @classmethod def for_output_contract( @@ -161,7 +184,11 @@ def for_output_contract( ) ensure_dev_client_renderers_registered() - for renderer_type in cls.__renderer_types__: + for owner in output_contract.__mro__: + renderer_type = cls.__registry__.get(owner) + if renderer_type is not None: + return McpDevOutputRendererBinding(output_contract, renderer_type) + for renderer_type in cls.declaration_types(): binding = renderer_type.binding_for_output_contract(output_contract) if binding is not None: return binding @@ -188,6 +215,18 @@ def render_bindings(cls) -> tuple["McpDevOutputRendererBinding", ...]: """Return additional output-contract bindings owned by this renderer.""" return () + @classmethod + def decode_payload(cls, payload: object, output_contract: type) -> object: + """Unmigrated families keep their existing presentation until cutover.""" + del output_contract + return payload + + @classmethod + def render_result(cls, response, options: McpDevOutputRenderOptions) -> str: + from openhcs.serialization.json import to_jsonable + + return cls.render_with_options(to_jsonable(response), options) + @classmethod def render(cls, response: JsonObject) -> str: raise NotImplementedError @@ -210,6 +249,16 @@ class McpDevOutputRendererBinding: renderer_type: type[McpDevOutputRenderer] render_function: McpDevOutputRenderFunction | None = None + def decode_payload(self, payload: object) -> object: + return self.renderer_type.decode_payload(payload, self.output_contract) + + def render_result(self, response, options: McpDevOutputRenderOptions) -> str: + if self.render_function is not None: + from openhcs.serialization.json import to_jsonable + + return self.render_function(to_jsonable(response)) + return self.renderer_type.render_result(response, options) + def configure_cli_parser(self, parser: argparse.ArgumentParser) -> None: self.renderer_type.render_options_type.configure_cli_parser(parser) @@ -232,6 +281,53 @@ def render_with_options( return self.renderer_type.render_with_options(response, options) +class McpDevTypedOutputRenderer(McpDevOutputRenderer): + """Shared contract descent for typed presentation members, never raw readers.""" + + @classmethod + def render_result(cls, response, options: McpDevOutputRenderOptions) -> str: + return cls.render_with_options(response, options) + + @classmethod + def render(cls, response) -> str: + return cls.render_with_options(response, cls.render_options_type()) + + @classmethod + def render_with_options(cls, response, options: McpDevOutputRenderOptions) -> str: + from openhcs.mcp.dev_client_core import McpDevToolBatchResponse + + decoded = McpDevToolBatchResponse.for_rendering(response) + payload = next( + (result.first_decoded_payload() for result in decoded.results), None + ) + lines = [ + "Result: " + if payload is None + else cls.render_payload_value(payload, options) + ] + lines.extend( + McpDiagnosticRenderer.typed_error_lines(decoded.diagnostic_errors()) + ) + return "\n".join(lines) + + @classmethod + def render_payload(cls, payload, options: McpDevOutputRenderOptions) -> str: + raise NotImplementedError + + @classmethod + def render_payload_value(cls, payload, options: McpDevOutputRenderOptions) -> str: + binding = McpDevOutputRenderer.for_output_contract(type(payload)) + if binding is None: + raise TypeError(f"No renderer declared for {type(payload).__name__}") + return binding.renderer_type.render_payload(payload, options) + + @classmethod + def decode_payload(cls, payload: object, output_contract: type) -> object: + if isinstance(payload, output_contract): + return payload + return dataclass_from_mapping(output_contract, payload) + + class McpDevPayloadProjection: """Small read helpers for dev-client JSON envelopes.""" @@ -352,15 +448,21 @@ def response_error_lines(cls, response: JsonObject) -> tuple[str, ...]: @staticmethod def error_lines(errors: tuple[Mapping[str, JsonValue], ...]) -> tuple[str, ...]: + return McpDiagnosticRenderer.typed_error_lines( + tuple(dataclass_from_mapping(AgentError, error) for error in errors) + ) + + @staticmethod + def typed_error_lines(errors) -> tuple[str, ...]: grouped_codes: dict[str, list[str]] = {} grouped_hints: dict[str, list[str]] = {} for error in errors: - message = McpDevPayloadProjection.text(error.get("message")) - hint = error.get("hint") + message = error.message + hint = error.hint hint_text = ( None if hint is None else McpDevPayloadProjection.quoted_text(hint) ) - code = McpDevPayloadProjection.text(error.get("code")) + code = error.code codes = grouped_codes.setdefault(message, []) if code not in codes: codes.append(code) @@ -373,7 +475,7 @@ def error_lines(errors: tuple[Mapping[str, JsonValue], ...]) -> tuple[str, ...]: lines: list[str] = [] for message, codes in tuple(grouped_codes.items())[:3]: code_text = codes[0] if len(codes) == 1 else ", ".join(codes) - line = f"- {code_text}: " f"{message}" + line = f"- {code_text}: {message}" hint_texts = grouped_hints.get(message, []) if len(hint_texts) == 1: line += f" hint={hint_texts[0]}" diff --git a/tests/unit/agent/test_mcp_dev_client_pipeline_results.py b/tests/unit/agent/test_mcp_dev_client_pipeline_results.py new file mode 100644 index 0000000000..7a4530c932 --- /dev/null +++ b/tests/unit/agent/test_mcp_dev_client_pipeline_results.py @@ -0,0 +1,566 @@ +"""Production command journeys with controlled wire receipts, never a runtime.""" + +from __future__ import annotations + +import ast +import asyncio +import json +import sys +from dataclasses import dataclass, replace +from pathlib import Path + +import pytest +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.execution import ( + ArtifactInputPlanSummary, + ArtifactMaterializationPathSummary, + ArtifactMaterializationPlanSummary, + ArtifactPlanInspection, + ArtifactPlanSummary, + CompiledStepPlanSummary, + ExecutionJobRef, + ExecutionJobStatus, + MainFlowMaterializationPlanSummary, + OrchestratorSessionRef, + SourceWorkspaceFileRecord, + SourceWorkspaceSummary, + ViewerStreamingPlanSummary, +) +from openhcs.agent.dto.pipeline import ( + FunctionSpecRef, + FunctionStepSpec, + PipelineRef, + PipelineSpec, + PipelineValidationResult, +) +from openhcs.core.streaming_config_declarations import ViewerType +from openhcs.mcp import dev_client +from openhcs.mcp.dev_client_commanding import CapabilityBackedCommandSpec +from openhcs.mcp.dev_client_core import ( + McpDevPayloadFailure, + McpDevServerSpec, + McpDevToolBatchResponse, + McpDevToolResult, +) +from openhcs.mcp.dev_client_rendering import McpDevOutputRenderer +from openhcs.mcp.dev_client_renderers.pipeline import PipelineArtifactPlanRenderer +from openhcs.serialization.json import to_jsonable + + +class ControlledWireSession: + """Only replace the wire peer; retain real framing, commands and rendering.""" + + def __init__(self, receipts, *, structured=True): + self.server_spec = McpDevServerSpec(sys.executable) + self.receipts = iter(receipts) + self.calls = [] + self.structured = structured + + async def call_tool(self, name, arguments, *, timeout_seconds): + self.calls.append((name, arguments, timeout_seconds)) + receipt = to_jsonable(next(self.receipts)) + if self.structured: + return {"isError": False, "structuredContent": receipt, "content": []} + return { + "isError": False, + "content": [{"type": "text", "text": json.dumps(receipt)}], + } + + +def batch(capability, value): + wire = {"isError": False, "structuredContent": to_jsonable(value), "content": []} + return McpDevToolBatchResponse.from_results( + McpDevServerSpec(sys.executable), + (McpDevToolResult.from_payload(capability.name, wire),), + ) + + +def args_for(*argv): + return dev_client._build_parser().parse_args(argv) + + +def plan_fixture(): + checkpoint = ArtifactMaterializationPlanSummary( + persistent_enabled=True, + persistent_backend="disk", + paths=tuple( + ArtifactMaterializationPathSummary( + str(index), f"image-{index}", (f"result-{index}.tif",) + ) + for index in range(4) + ), + filename_uses_source_identity=True, + note="receipt retained", + ) + workspace = SourceWorkspaceSummary( + file_count=7, + files=tuple( + SourceWorkspaceFileRecord( + f"virtual-{index}", + f"/full/virtual-{index}", + f"physical-{index}", + {"new_component": {"identity": index}}, + ) + for index in range(7) + ), + axis_file_counts={"A": 7}, + ) + step = CompiledStepPlanSummary( + 1, + "Owned step", + "A", + "result-dir", + execution_groups=("g",), + main_flow_axis_persistence_enabled=True, + main_flow_materialization=MainFlowMaterializationPlanSummary( + "result-dir", "disk", "plate-root", "sub-dir" + ), + viewer_streaming=( + ViewerStreamingPlanSummary( + "napari", ViewerType.NAPARI, "memory", {"new_option": [1, 2]} + ), + ), + artifact_inputs=( + ArtifactInputPlanSummary( + "input", + "image", + "input-path", + ("g",), + source_step_id=2, + source_step_scope_id="scope-2", + ), + ), + artifact_outputs=( + ArtifactPlanSummary( + "output", "image", "output-path", ("g",), materialization=checkpoint + ), + ), + ) + return ArtifactPlanInspection( + schema_version=SCHEMA_VERSION, + plate_path="source-only-plate", + axis_count=1, + axes=("A",), + step_count=1, + steps=(step,), + worker_assignments={"worker": ["A"]}, + source_workspace=workspace, + progress_event_count=3, + ) + + +@pytest.mark.parametrize("structured", [True, False]) +def test_draft_journey_decodes_once_then_renders_real_command(structured, monkeypatch): + ref = PipelineRef("pipeline-1", "openhcs://pipelines/pipeline-1") + spec = PipelineSpec( + SCHEMA_VERSION, + "pipeline-1", + "config-1", + ( + FunctionStepSpec( + "step-1", + "Count cells", + (FunctionSpecRef("function-1", {"extension": [1, 2]}),), + ), + ), + ) + validation = PipelineValidationResult( + schema_version=SCHEMA_VERSION, + valid=False, + pipeline_ref=ref, + errors=( + AgentError( + "missing_function_kwargs", + "Missing required agent kwargs: threshold.", + "required agent kwargs: threshold.", + ), + ), + warnings=(AgentWarning("note", "Pipeline is small."),), + ) + source = RenderedSource( + SCHEMA_VERSION, "Pipeline", "pipeline_steps = [FunctionStep(...)]\n" + ) + peer = ControlledWireSession((ref, spec, validation, source), structured=structured) + args = args_for("draft-pipeline-step", "function-1", "--max-source-chars", "10") + command = dev_client.McpDevCommandSpec.for_name(args.command) + import openhcs.mcp.dev_client_rendering as rendering + + decoded_contracts = [] + decode = rendering.dataclass_from_mapping + + def observed_decode(contract, payload): + decoded_contracts.append(contract) + return decode(contract, payload) + + monkeypatch.setattr(rendering, "dataclass_from_mapping", observed_decode) + response = asyncio.run(command.run_session(peer, args)) + assert [call[0] for call in peer.calls] == [ + agent_capabilities.create_pipeline.name, + agent_capabilities.add_function_step.name, + agent_capabilities.validate_pipeline.name, + agent_capabilities.render_pipeline_source.name, + ] + assert peer.calls[1][1]["pipeline_id"] == ref.pipeline_id + assert isinstance(response.results[1].payloads[0], PipelineSpec) + assert response.results[1].payloads[0].steps[0].functions[0].kwargs == { + "extension": [1, 2] + } + before = to_jsonable(response) + render = command.render_result(response, args) + assert "id=pipeline-1 valid=False steps=1" in render + assert "functions=function-1" in render + assert "missing_function_kwargs" in render and "threshold" in render + assert "note: Pipeline is small." in render + assert "Next: function function-1" in render and "Retry shape:" in render + assert "truncated" in render + assert to_jsonable(response) == before + assert decoded_contracts == [ + PipelineRef, + PipelineSpec, + PipelineValidationResult, + RenderedSource, + ] + assert command.render_result( + response, replace_namespace(args, json=True) + ) == json.dumps(before, indent=2, sort_keys=True) + + +def replace_namespace(args, **values): + import argparse + + return argparse.Namespace(**(vars(args) | values)) + + +def test_nested_artifact_contract_and_receipt_survive_compact_limits(): + original = plan_fixture() + response = batch(agent_capabilities.inspect_pipeline_source_artifact_plan, original) + decoded = response.results[0].payloads[0] + assert isinstance(decoded, ArtifactPlanInspection) + assert isinstance(decoded.source_workspace.files[6], SourceWorkspaceFileRecord) + assert decoded.steps[0].viewer_streaming[0].viewer_type is ViewerType.NAPARI + assert decoded.steps[0].artifact_outputs[0].materialization.paths[ + 3 + ].candidate_paths == ("result-3.tif",) + assert to_jsonable(decoded) == to_jsonable(original) + args = args_for( + "artifact-plan", "source-only-plate", "--source-text", "pipeline_steps = []" + ) + rendered = dev_client.McpDevCommandSpec.for_name(args.command).render_result( + response, args + ) + for fact in ( + "source-only-plate", + "axes=1", + "progress_events=3", + "new_component", + "source_scope=scope-2", + "backend=disk", + "sub_dir=sub-dir", + "viewer=napari", + "result-2.tif", + "truncated=1", + "receipt retained", + ): + assert fact in rendered + assert "physical-6" not in rendered + assert len(decoded.source_workspace.files) == 7 + assert len(decoded.steps[0].artifact_outputs[0].materialization.paths) == 4 + assert to_jsonable(decoded) == to_jsonable(original) + + +@pytest.mark.parametrize("waited", [True, False]) +def test_execute_journey_preserves_native_variants_progress_and_errors(waited): + session = OrchestratorSessionRef( + session_id="session-1", + schema_version=SCHEMA_VERSION, + uri="openhcs://sessions/1", + ) + identity = dict( + schema_version=SCHEMA_VERSION, + job_id="job-1", + session_id="session-1", + kind="execute", + status="failed" if waited else "queued", + uri="openhcs://jobs/1", + server_execution_id="execution-1", + ) + job = ( + ExecutionJobStatus( + **identity, + response={"native_extension": {"retain": [1, 2]}, "status": "failed"}, + progress=ExecutionProgressObservation( + 3, {"phase": "processing", "extension": {"done": 2}} + ), + errors=( + AgentError("execution_failed", "Failure retained", "Inspect receipt"), + ), + warnings=(AgentWarning("wait", "Deadline reached"),), + ) + if waited + else ExecutionJobRef(**identity) + ) + peer = ControlledWireSession((session, job)) + args = args_for( + "execute-source", + "source-only-plate", + "--source-text", + "pipeline_steps = []", + "--wait" if waited else "--no-wait", + ) + command = dev_client.McpDevCommandSpec.for_name(args.command) + response = asyncio.run(command.run_session(peer, args)) + decoded = response.results[1].payloads[0] + assert type(decoded) is type(job) + assert to_jsonable(decoded) == to_jsonable(job) + assert peer.calls[1][1]["session_id"] == "session-1" + rendered = command.render_result(response, args) + assert ( + "id=session-1" in rendered + and "id=job-1" in rendered + and "server_execution=execution-1" in rendered + ) + if waited: + assert decoded.progress.sequence == 3 + assert decoded.progress.event["extension"]["done"] == 2 + for fact in ( + "native_extension", + "processing", + "Failure retained", + "Inspect receipt", + "Deadline reached", + ): + assert fact in rendered + assert response.has_errors() + else: + assert not response.has_errors() + + +@pytest.mark.parametrize( + "broken", + [ + {"schema_version": SCHEMA_VERSION, "plate_path": "source-only", "steps": [42]}, + { + "schema_version": SCHEMA_VERSION, + "plate_path": "source-only", + "axis_count": True, + }, + { + "schema_version": SCHEMA_VERSION, + "plate_path": "source-only", + "source_workspace": {"files": [{"virtual_path": "v"}]}, + }, + { + "schema_version": SCHEMA_VERSION, + "plate_path": "source-only", + "undeclared": "not metadata", + }, + { + "schema_version": SCHEMA_VERSION, + "ok": False, + "errors": [ + { + "code": "server_stale", + "message": "Reconnect first", + "hint": "No retry", + } + ], + "restart_required": True, + }, + ], +) +def test_malformed_contract_is_explicit_and_keeps_entire_receipt(broken): + response = batch(agent_capabilities.inspect_pipeline_source_artifact_plan, broken) + failed = response.results[0].payloads[0] + assert isinstance(failed, McpDevPayloadFailure) + assert failed.receipt == broken + assert to_jsonable(failed) == broken + assert response.has_errors() + rendered = PipelineArtifactPlanRenderer.render(response) + assert "mcp_payload_invalid" in rendered and "unavailable" in rendered + if "errors" in broken: + assert "Reconnect first" in rendered and "No retry" in rendered + + +def test_not_run_missing_transport_and_mcp_errors_are_distinct(): + args = args_for("draft-pipeline-step", "function-1") + command = dev_client.McpDevCommandSpec.for_name(args.command) + empty = McpDevToolBatchResponse.from_results(McpDevServerSpec(sys.executable), ()) + assert "valid=" in command.render_result(empty, args) + missing = replace( + empty, + results=( + McpDevToolResult(agent_capabilities.validate_pipeline.name, False, ()), + ), + ) + assert "mcp_payload_missing" in command.render_result(missing, args) + tool_error = replace( + empty, + results=( + McpDevToolResult(agent_capabilities.validate_pipeline.name, True, ()), + ), + ) + assert "mcp_tool_error" in command.render_result(tool_error, args) + failure = command.transport_failure_response( + McpDevServerSpec(sys.executable), + command.execution_phase, + RuntimeError("connection lost"), + server_stderr_tail="full retained stderr", + ) + rendered = command.render_result(failure, args) + assert "connection lost" in rendered and "mcp_transport_failed" in rendered + assert failure.errors[0].server_stderr_tail == "full retained stderr" + + +def test_single_declaration_extension_uses_output_mro_without_consumer_edits(): + @dataclass(frozen=True, slots=True, kw_only=True) + class ExtendedInspection(ArtifactPlanInspection): + extension_fact: str = "retained" + + binding = McpDevOutputRenderer.for_output_contract(ExtendedInspection) + assert binding.renderer_type is PipelineArtifactPlanRenderer + value = binding.decode_payload( + to_jsonable( + ExtendedInspection(schema_version=SCHEMA_VERSION, plate_path="extended") + ) + ) + assert type(value) is ExtendedInspection and value.extension_fact == "retained" + assert "plate=extended" in binding.renderer_type.render_payload_value( + value, binding.renderer_type.render_options_type() + ) + + +def test_cooperative_diamond_renderer_identity_is_visited_once(): + @dataclass(frozen=True, slots=True) + class NewResult: + value: str + + events = [] + + class Left(PipelineArtifactPlanRenderer): + @classmethod + def render_payload(cls, payload, options): + events.append("left") + return super().render_payload(payload, options) + + class Right(PipelineArtifactPlanRenderer): + @classmethod + def render_payload(cls, payload, options): + events.append("right") + return payload.value + + class Diamond(Left, Right): + output_contract = NewResult + + binding = McpDevOutputRenderer.for_output_contract(NewResult) + value = binding.decode_payload({"value": "one-declaration"}) + assert ( + binding.renderer_type.render_payload_value( + value, binding.renderer_type.render_options_type() + ) + == "one-declaration" + ) + assert events == ["left", "right"] + assert tuple(McpDevOutputRenderer.declaration_types()).count(Diamond) == 1 + # Registry views are projections of declaration identity, not a second roster. + assert McpDevOutputRenderer.__registry__[NewResult] is Diamond + + +def test_pipeline_guard_forbids_raw_reader_reintroduction(): + import openhcs.mcp.dev_client_renderers.pipeline as pipeline + + tree = ast.parse(Path(pipeline.__file__).read_text()) + for node in ast.walk(tree): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): + assert node.func.attr not in { + "get", + "nested_mapping", + "sequence_of_mappings", + "first_tool_payload", + "tool_payload", + } + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + assert node.func.id not in { + "isinstance", + "getattr", + "hasattr", + "optional_int", + "optional_bool", + } + + +def test_submission_declaration_preserves_advertised_external_contract(): + from openhcs.agent.capabilities import require_agent_type_contract + from openhcs.mcp.server import _mcp_tool_meta + + for capability in ( + agent_capabilities.submit_compile, + agent_capabilities.submit_pipeline_execution, + ): + assert capability.output_contract_types == (ExecutionJobRef, ExecutionJobStatus) + assert ( + require_agent_type_contract(capability.output_contract) is ExecutionJobRef + ) + assert _mcp_tool_meta(capability) == { + "openhcs/outputContract": "ExecutionJobRef" + } + + +def test_typed_diagnostics_do_not_reserialize_or_rescan_error_records(monkeypatch): + import openhcs.mcp.dev_client_core as core + + error = AgentError("nested_failure", "Retained typed diagnosis", "Retained hint") + response = batch( + agent_capabilities.validate_pipeline, + PipelineValidationResult( + schema_version=SCHEMA_VERSION, + valid=False, + pipeline_ref=PipelineRef("p", "openhcs://pipelines/p"), + errors=(error,), + ), + ) + + def forbidden_serialization(value): + raise AssertionError("Typed diagnostics must not round-trip through JSON") + + monkeypatch.setattr(core, "to_jsonable", forbidden_serialization) + assert response.has_errors() + assert response.results[0].agent_error_codes() == ("nested_failure",) + assert response.diagnostic_errors()[0] == error + assert "Retained hint" in dev_client.McpDevCommandSpec.for_name( + "draft-pipeline-step" + ).render_result(response, args_for("draft-pipeline-step", "function-1")) + + +@pytest.mark.parametrize("json_output", [False, True]) +def test_actual_cli_main_renders_without_runtime(monkeypatch, capsys, json_output): + response = batch( + agent_capabilities.inspect_pipeline_source_artifact_plan, plan_fixture() + ) + + async def controlled_entry(args): + assert args.command == "artifact-plan" + return response + + monkeypatch.setattr(dev_client, "_run_async", controlled_entry) + argv = [ + "artifact-plan", + "source-only-plate", + "--source-text", + "pipeline_steps = []", + ] + if json_output: + argv.append("--json") + assert dev_client.main(argv) == 0 + printed = capsys.readouterr().out + if json_output: + assert json.loads(printed) == to_jsonable(response) + else: + assert "Owned step" in printed and "new_component" in printed diff --git a/tests/unit/agent/test_mcp_server.py b/tests/unit/agent/test_mcp_server.py index 516adb0339..4c40d91218 100644 --- a/tests/unit/agent/test_mcp_server.py +++ b/tests/unit/agent/test_mcp_server.py @@ -4668,6 +4668,13 @@ def test_mcp_dev_client_authoring_context_renders_bounded_content(): assert "..." in rendered +def _dev_client_server_fixture(): + from openhcs.mcp.dev_client_core import McpDevServerIdentity, McpDevServerSpec + from openhcs.serialization.json import to_jsonable + + return to_jsonable(McpDevServerIdentity.from_spec(McpDevServerSpec(sys.executable))) + + def test_mcp_dev_client_draft_pipeline_step_command_renders_composite_summary(): if importlib.util.find_spec("mcp") is None: return @@ -4690,6 +4697,7 @@ def test_mcp_dev_client_draft_pipeline_step_command_renders_composite_summary(): ) ) response = { + "server": _dev_client_server_fixture(), "errors": [], "results": [ { @@ -4707,7 +4715,9 @@ def test_mcp_dev_client_draft_pipeline_step_command_renders_composite_summary(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "pipeline_id": "pipeline-1", + "pipeline_config_id": "config-1", "steps": [ { "step_id": "step-1", @@ -4722,7 +4732,6 @@ def test_mcp_dev_client_draft_pipeline_step_command_renders_composite_summary(): ], } ], - "errors": [], } ], }, @@ -4731,7 +4740,12 @@ def test_mcp_dev_client_draft_pipeline_step_command_renders_composite_summary(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "valid": True, + "pipeline_ref": { + "pipeline_id": "pipeline-1", + "uri": "openhcs://pipelines/pipeline-1", + }, "warnings": [{"code": "note", "message": "Pipeline is small."}], "errors": [], } @@ -4742,9 +4756,9 @@ def test_mcp_dev_client_draft_pipeline_step_command_renders_composite_summary(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "title": "Pipeline", "source": "pipeline_steps = [\\n FunctionStep(...)\\n]\\n", - "errors": [], } ], }, @@ -4783,6 +4797,7 @@ def test_mcp_dev_client_draft_pipeline_step_suggests_missing_kwargs_repair(): ) ) response = { + "server": _dev_client_server_fixture(), "errors": [], "results": [ { @@ -4800,7 +4815,9 @@ def test_mcp_dev_client_draft_pipeline_step_suggests_missing_kwargs_repair(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "pipeline_id": "pipeline-1", + "pipeline_config_id": "config-1", "steps": [ { "step_id": "step-1", @@ -4816,7 +4833,6 @@ def test_mcp_dev_client_draft_pipeline_step_suggests_missing_kwargs_repair(): ], } ], - "errors": [], } ], }, @@ -4825,7 +4841,12 @@ def test_mcp_dev_client_draft_pipeline_step_suggests_missing_kwargs_repair(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "valid": False, + "pipeline_ref": { + "pipeline_id": "pipeline-1", + "uri": "openhcs://pipelines/pipeline-1", + }, "warnings": [], "errors": [ { @@ -4918,6 +4939,7 @@ def test_mcp_dev_client_artifact_plan_command_renders_compact_summary(): ) ) response = { + "server": _dev_client_server_fixture(), "errors": [], "results": [ { @@ -4925,6 +4947,7 @@ def test_mcp_dev_client_artifact_plan_command_renders_compact_summary(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "plate_path": "/tmp/example-plate", "axes": ["A01"], "axis_count": 1, @@ -4938,6 +4961,7 @@ def test_mcp_dev_client_artifact_plan_command_renders_compact_summary(): "files": [ { "virtual_path": "A01_s001_w1_z001_t001.tif", + "full_virtual_path": "/virtual/A01_s001_w1_z001_t001.tif", "source_path": "/tmp/source/A01_w1.tif", "source_metadata": { "well": "A01", @@ -4952,6 +4976,7 @@ def test_mcp_dev_client_artifact_plan_command_renders_compact_summary(): "step_index": 0, "step_name": "Count cells", "axis_id": "A01", + "output_dir": None, "execution_groups": [None], "artifact_inputs": [ { @@ -4985,7 +5010,7 @@ def test_mcp_dev_client_artifact_plan_command_renders_compact_summary(): "paths": [ { "group_key": None, - "base_path": ( + "shared_output_stem": ( "/tmp/example-plate_openhcs/" "images_results/" "A01_cell_counts_step0.roi.zip" @@ -5073,6 +5098,7 @@ def test_mcp_dev_client_artifact_plan_explains_empty_source_workspace(): ) ) response = { + "server": _dev_client_server_fixture(), "errors": [], "results": [ { @@ -5080,6 +5106,7 @@ def test_mcp_dev_client_artifact_plan_explains_empty_source_workspace(): "mcp_error": False, "payloads": [ { + "schema_version": "openhcs.agent.v1", "plate_path": "/tmp/example-plate", "axes": ["A01"], "axis_count": 1, @@ -5167,7 +5194,7 @@ async def fake_call_tool(session, call, timeout_seconds): "uri": "openhcs://execution/sessions/session-1", }, ), - ) + ).decoded_for_rendering() return dev_client.McpDevToolResult( tool=call.name, mcp_error=False, @@ -5176,13 +5203,14 @@ async def fake_call_tool(session, call, timeout_seconds): "schema_version": "openhcs.agent.v1", "session_id": "session-1", "job_id": "job-1", + "uri": "openhcs://execution/jobs/job-1", "kind": "execute", "status": "complete", "server_execution_id": "exec-1", "response": {"status": "complete", "completed": True}, }, ), - ) + ).decoded_for_rendering() monkeypatch.setattr(knowledge_pipeline, "call_mcp_tool", fake_call_tool) From c47b1f6a95c223e7be3d23fe3b3e76955716aa73 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 00:23:42 -0400 Subject: [PATCH 02/10] refactor(mcp): share declared nullable presentation composition --- openhcs/mcp/dev_client_renderers/pipeline.py | 82 +++++++++++++------- openhcs/mcp/dev_client_rendering.py | 9 +++ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/openhcs/mcp/dev_client_renderers/pipeline.py b/openhcs/mcp/dev_client_renderers/pipeline.py index af26b234ed..c31fc530d1 100644 --- a/openhcs/mcp/dev_client_renderers/pipeline.py +++ b/openhcs/mcp/dev_client_renderers/pipeline.py @@ -269,20 +269,23 @@ def _step_lines(cls, steps: tuple[CompiledStepPlanSummary, ...]) -> list[str]: lines.append( f"- {step.step_index}: {step.step_name} axis={step.axis_id} groups={_sequence_text(step.execution_groups)}" ) - if step.main_flow_axis_persistence_enabled is not None: - lines.append( - " main-flow axis persistence: " - + ( - "enabled" - if step.main_flow_axis_persistence_enabled - else "runtime-only" - ) + lines.extend( + cls.optional_lines( + step.main_flow_axis_persistence_enabled, + lambda enabled: ( + " main-flow axis persistence: " + + ("enabled" if enabled else "runtime-only"), + ), ) - checkpoint = step.main_flow_materialization - if checkpoint is not None: - lines.append( - f" main-flow checkpoint: backend={checkpoint.backend} output_dir={checkpoint.output_dir} sub_dir={checkpoint.sub_dir}" + ) + lines.extend( + cls.optional_lines( + step.main_flow_materialization, + lambda checkpoint: ( + f" main-flow checkpoint: backend={checkpoint.backend} output_dir={checkpoint.output_dir} sub_dir={checkpoint.sub_dir}", + ), ) + ) for viewer in step.viewer_streaming: line = f" viewer stream: viewer={viewer.viewer_type.value} config={viewer.config_key} backend={viewer.backend}" if viewer.effective_config: @@ -294,8 +297,11 @@ def _step_lines(cls, steps: tuple[CompiledStepPlanSummary, ...]) -> list[str]: lines.append( f" artifact {artifact.name}: kind={artifact.kind} path={artifact.path} groups={_sequence_text(artifact.group_keys)}" ) - if artifact.materialization is not None: - lines.extend(cls._materialization_lines(artifact.materialization)) + lines.extend( + cls.optional_lines( + artifact.materialization, cls._materialization_lines + ) + ) if step.truncated_artifact_input_count > 0: lines.append( f" artifact input ... truncated={step.truncated_artifact_input_count}" @@ -306,18 +312,25 @@ def _step_lines(cls, steps: tuple[CompiledStepPlanSummary, ...]) -> list[str]: ) return lines - @staticmethod - def _artifact_input_line(artifact: ArtifactInputPlanSummary) -> str: + @classmethod + def _artifact_input_line(cls, artifact: ArtifactInputPlanSummary) -> str: source_parts: list[str] = [] - if artifact.source_step_id is not None: - source_parts.append(f"source_step={artifact.source_step_id}") - if artifact.source_step_scope_id is not None: - source_parts.append(f"source_scope={artifact.source_step_scope_id}") + source_parts.extend( + cls.optional_lines( + artifact.source_step_id, lambda value: (f"source_step={value}",) + ) + ) + source_parts.extend( + cls.optional_lines( + artifact.source_step_scope_id, lambda value: (f"source_scope={value}",) + ) + ) suffix = f" {' '.join(source_parts)}" if source_parts else "" return f" artifact input {artifact.name}: kind={artifact.kind} path={artifact.path} groups={_sequence_text(artifact.group_keys)}{suffix}" - @staticmethod + @classmethod def _materialization_lines( + cls, materialization: ArtifactMaterializationPlanSummary, ) -> list[str]: mode = ( @@ -328,10 +341,17 @@ def _materialization_lines( else "explicit" ) parts = [mode, f"persistent={materialization.persistent_enabled}"] - if materialization.persistent_backend is not None: - parts.append(f"backend={materialization.persistent_backend}") - if materialization.analysis_output_dir is not None: - parts.append(f"analysis_dir={materialization.analysis_output_dir}") + parts.extend( + cls.optional_lines( + materialization.persistent_backend, lambda value: (f"backend={value}",) + ) + ) + parts.extend( + cls.optional_lines( + materialization.analysis_output_dir, + lambda value: (f"analysis_dir={value}",), + ) + ) if materialization.filename_uses_source_identity: parts.append("source-identity-filenames") if materialization.runtime_metadata_can_refine_paths: @@ -345,8 +365,11 @@ def _materialization_lines( lines.append( f" candidates ... truncated={len(materialization.paths) - 3}" ) - if materialization.note is not None: - lines.append(f" note: {materialization.note}") + lines.extend( + cls.optional_lines( + materialization.note, lambda value: (f" note: {value}",) + ) + ) return lines @@ -391,8 +414,9 @@ def render_payload( "Response: " + " ".join(f"{key}={value}" for key, value in payload.response.items()) ) - if payload.progress is not None: - lines.append(f"Progress: {payload.progress}") + lines.extend( + cls.optional_lines(payload.progress, lambda value: (f"Progress: {value}",)) + ) _append_messages(lines, (), payload.warnings) return "\n".join(lines) diff --git a/openhcs/mcp/dev_client_rendering.py b/openhcs/mcp/dev_client_rendering.py index 93d053166f..dc1c08c028 100644 --- a/openhcs/mcp/dev_client_rendering.py +++ b/openhcs/mcp/dev_client_rendering.py @@ -284,6 +284,15 @@ def render_with_options( class McpDevTypedOutputRenderer(McpDevOutputRenderer): """Shared contract descent for typed presentation members, never raw readers.""" + @staticmethod + def optional_lines(value, render_lines): + """Compose a nullable declared fact without repeating omission policy. + + None is the native absence fact; false, zero and empty strings remain + present. Members own only the presentation of a present value. + """ + return () if value is None else render_lines(value) + @classmethod def render_result(cls, response, options: McpDevOutputRenderOptions) -> str: return cls.render_with_options(response, options) From 018caf90c3ed19ef7248a09156a8e19dbadfaa0e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 00:26:04 -0400 Subject: [PATCH 03/10] test(mcp): prove declared extension ingress and nullable fact semantics --- openhcs/mcp/dev_client_rendering.py | 10 +++++-- .../test_mcp_dev_client_pipeline_results.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/openhcs/mcp/dev_client_rendering.py b/openhcs/mcp/dev_client_rendering.py index dc1c08c028..fb12c1afeb 100644 --- a/openhcs/mcp/dev_client_rendering.py +++ b/openhcs/mcp/dev_client_rendering.py @@ -4,10 +4,10 @@ import argparse import json -from collections.abc import Callable, Mapping +from collections.abc import Callable, Mapping, Sequence from dataclasses import asdict, dataclass from enum import Enum -from typing import ClassVar, TypeAlias +from typing import ClassVar, TypeAlias, TypeVar from metaclass_registry import AutoRegisterMeta from python_introspect import dataclass_from_mapping @@ -20,6 +20,7 @@ from openhcs.agent.dto.common import AgentError, JsonObject, JsonValue DEFAULT_CODE_DOCUMENT_MAX_CHARS = 2_000 +PresentationValue = TypeVar("PresentationValue") class WidgetTreeOutputFormat(str, Enum): @@ -285,7 +286,10 @@ class McpDevTypedOutputRenderer(McpDevOutputRenderer): """Shared contract descent for typed presentation members, never raw readers.""" @staticmethod - def optional_lines(value, render_lines): + def optional_lines( + value: PresentationValue | None, + render_lines: Callable[[PresentationValue], Sequence[str]], + ) -> Sequence[str]: """Compose a nullable declared fact without repeating omission policy. None is the native absence fact; false, zero and empty strings remain 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 7a4530c932..a20567e596 100644 --- a/tests/unit/agent/test_mcp_dev_client_pipeline_results.py +++ b/tests/unit/agent/test_mcp_dev_client_pipeline_results.py @@ -420,6 +420,8 @@ def test_not_run_missing_transport_and_mcp_errors_are_distinct(): def test_single_declaration_extension_uses_output_mro_without_consumer_edits(): + from openhcs.agent.capabilities import InspectPipelineSourceArtifactPlanCapability + @dataclass(frozen=True, slots=True, kw_only=True) class ExtendedInspection(ArtifactPlanInspection): extension_fact: str = "retained" @@ -436,6 +438,32 @@ class ExtendedInspection(ArtifactPlanInspection): value, binding.renderer_type.render_options_type() ) + # One capability declaration selects the new output at real wire ingress; + # presentation inherits the existing owner, without adding a roster entry. + class ExtensionCapability(InspectPipelineSourceArtifactPlanCapability): + name = "openhcs_s1_extension_inspection" + cli_command = "s1-extension-inspection" + output_contract = ExtendedInspection + + response = batch(ExtensionCapability.to_spec(), value) + assert type(response.results[0].first_decoded_payload()) is ExtendedInspection + assert "plate=extended" in PipelineArtifactPlanRenderer.render(response) + + +@pytest.mark.parametrize("value", [None, False, 0, ""]) +def test_shared_optional_presentation_preserves_native_absence(value): + from openhcs.mcp.dev_client_rendering import McpDevTypedOutputRenderer + + observed = [] + + def present(fact): + observed.append(fact) + return (str(fact),) + + lines = McpDevTypedOutputRenderer.optional_lines(value, present) + assert lines == (() if value is None else (str(value),)) + assert observed == ([] if value is None else [value]) + def test_cooperative_diamond_renderer_identity_is_visited_once(): @dataclass(frozen=True, slots=True) From 3c37aac61b6b77d6b8eb3d52aaf2a221f78b96b8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 00:29:21 -0400 Subject: [PATCH 04/10] docs(mcp): record tested S1 checkpoint and bounded guard coverage --- docs/refactor/S1-mcp-result-owners.rst | 41 ++++++++++++++++++ .../receipts/S1-pipeline-20261001.tar.gz | Bin 0 -> 312386 bytes .../test_mcp_dev_client_pipeline_results.py | 13 ++++-- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 docs/refactor/receipts/S1-pipeline-20261001.tar.gz diff --git a/docs/refactor/S1-mcp-result-owners.rst b/docs/refactor/S1-mcp-result-owners.rst index 003afa7edb..703f721686 100644 --- a/docs/refactor/S1-mcp-result-owners.rst +++ b/docs/refactor/S1-mcp-result-owners.rst @@ -145,6 +145,47 @@ compact/JSON entrypoints with a controlled wire response, no runtime launch. Native server metadata remains exactly openhcs/outputContract=ExecutionJobRef. Typed error interpretation is tested with serialization forbidden. +After normal integration of parent main b31807d9de067e18d212bdf9353d05099757fc74: +29 passed (22 new, seven existing), 17.17s wall, 310812 KiB RSS. Single-declaration +extension now also exercises real capability ingress and generated command +rendering. Four nullable-fact cases prove false, zero and empty strings are not +silently omitted as absence. Original R0 found eight repeated foreign optional +probes; replaced all nine nullable composition sites with the typed ancestor's +shared optional_lines policy, not local variable rebinding or guard exceptions. +Leaf owners format present facts; the native None absence fact is unchanged. + +Original R0 at agent-comms 3b03785f45df2ef5dc62ba6aed99294192ecbb01, +head 018caf90c, base b31807d9: openhcs PASS, 26.53s/87592 KiB. No increased +measure. Original failed R0 remains archived. Syntax/correctness Ruff subset +E9/F63/F7/F82 passes; broad inherited lint debt was not mass-edited. +Whole renderer snapshot after pipeline: 5962 code lines, literal_key_get +157.33/1000 (938 reads), isinstance_call 19.96/1000 (119), None 26.00/1000 +(155). Pipeline weighted debt 100 -> 11; 502 -> 400 code lines. This measures +remaining family debt honestly and does not count shuffling as closure. + +Unchanged R1, authoritative NRA source 0844525ecaba93e090a064a4ae4914466b2dae60, +base b31807d9, current source head c47b1f6a9: INCOMPLETE, not PASS. First bounded +attempt could not mmap Git's pack under a 512 MiB address-space limit. Retried +the same roots/revisions/tool with bounded Git mapping windows (8m/128m), not +changed detector inputs: materialized recorded source/dependency context, then +ScanDeadlineExceeded during parse_python_module at 55.000s. Wall 56.20s, +263856 KiB RSS. No comparison or descent/proof result returned. Roots remain +openhcs, scripts, benchmark plus all recorded external submodule Python context; +report scope remains changed surviving production files. No exception/waiver, +no substituted scoped result, no full-package/global NRA proof claim. Continuing +source progress does not repair or claim completion of this independent tool. +Owned persistent source snapshot scratch: worktree/.s1-r1-scratch; original tool +TemporaryDirectory cleaned its snapshots on the caught deadline exit. + +R0 scripts and benchmark also PASS unchanged (empty changed Python scope). +Shared-owner regression shard: 29 existing knowledge/architecture/function/ +authoring/tool-list/code-document command cases PASS, 28.04s/284140 KiB, +provider-free, no runtime. These are behavior checks, not full remaining-family +descent closure. Evidence archive: receipts/S1-pipeline-20261001.tar.gz contains +original failures, subsequent passing logs/XML, full guard outputs and census. +It excludes compiler/build products. Regenerate it after further owned checks; +owned terminal scratch is disposable once the archive is verified. + Original failures remain in the owned output archive: source-first import order collection failure; JsonValue annotation namespace failure; scoped registry/MRO failures; five existing fixture failures. Existing fixture updates supply missing diff --git a/docs/refactor/receipts/S1-pipeline-20261001.tar.gz b/docs/refactor/receipts/S1-pipeline-20261001.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..34d4f8e39ec607944df198b90d8dca84665013b9 GIT binary patch literal 312386 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 Rt9 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

y@wx< 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