runsight-core 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. {runsight_core-0.2.2 → runsight_core-0.2.3}/PKG-INFO +1 -1
  2. {runsight_core-0.2.2 → runsight_core-0.2.3}/pyproject.toml +1 -1
  3. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/_registry.py +10 -1
  4. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/code.py +1 -0
  5. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/dispatch.py +1 -0
  6. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/gate.py +1 -0
  7. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/linear.py +1 -0
  8. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/loop.py +1 -0
  9. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/synthesize.py +1 -0
  10. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/workflow_block.py +93 -10
  11. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/parser.py +15 -97
  12. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core.egg-info/PKG-INFO +1 -1
  13. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core.egg-info/SOURCES.txt +1 -0
  14. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run219_auto_registration.py +38 -0
  15. runsight_core-0.2.3/tests/test_run774_workflow_block_builder_path.py +189 -0
  16. {runsight_core-0.2.2 → runsight_core-0.2.3}/README.md +0 -0
  17. {runsight_core-0.2.2 → runsight_core-0.2.3}/setup.cfg +0 -0
  18. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/__init__.py +0 -0
  19. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/artifacts.py +0 -0
  20. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/__init__.py +0 -0
  21. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/base.py +0 -0
  22. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/contract.py +0 -0
  23. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/custom.py +0 -0
  24. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/deterministic/__init__.py +0 -0
  25. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/deterministic/linguistic.py +0 -0
  26. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/deterministic/performance.py +0 -0
  27. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/deterministic/string.py +0 -0
  28. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/deterministic/structural.py +0 -0
  29. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/registry.py +0 -0
  30. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/assertions/scoring.py +0 -0
  31. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/__init__.py +0 -0
  32. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/_helpers.py +0 -0
  33. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/base.py +0 -0
  34. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/blocks/registry.py +0 -0
  35. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/budget_enforcement.py +0 -0
  36. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/conditions/__init__.py +0 -0
  37. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/conditions/engine.py +0 -0
  38. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/eval/__init__.py +0 -0
  39. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/eval/runner.py +0 -0
  40. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/__init__.py +0 -0
  41. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/credentials.py +0 -0
  42. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/envelope.py +0 -0
  43. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/errors.py +0 -0
  44. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/handlers.py +0 -0
  45. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/harness.py +0 -0
  46. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/interceptors.py +0 -0
  47. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/ipc.py +0 -0
  48. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/ipc_models.py +0 -0
  49. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/pool.py +0 -0
  50. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/worker.py +0 -0
  51. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/worker_proxies.py +0 -0
  52. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/worker_support.py +0 -0
  53. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/isolation/wrapper.py +0 -0
  54. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/llm/__init__.py +0 -0
  55. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/llm/client.py +0 -0
  56. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/llm/model_catalog.py +0 -0
  57. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/memory/__init__.py +0 -0
  58. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/memory/budget.py +0 -0
  59. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/memory/token_counting.py +0 -0
  60. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/memory/windowing.py +0 -0
  61. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/observer.py +0 -0
  62. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/primitives.py +0 -0
  63. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/py.typed +0 -0
  64. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/runner.py +0 -0
  65. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/security.py +0 -0
  66. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/state.py +0 -0
  67. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/tools/__init__.py +0 -0
  68. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/tools/_catalog.py +0 -0
  69. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/tools/contract.py +0 -0
  70. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/tools/delegate.py +0 -0
  71. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/tools/file_io.py +0 -0
  72. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/tools/http.py +0 -0
  73. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/workflow.py +0 -0
  74. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/__init__.py +0 -0
  75. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/discovery/__init__.py +0 -0
  76. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/discovery/_assertion.py +0 -0
  77. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/discovery/_base.py +0 -0
  78. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/discovery/_soul.py +0 -0
  79. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/discovery/_tool.py +0 -0
  80. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/discovery/_workflow.py +0 -0
  81. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/registry.py +0 -0
  82. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core/yaml/schema.py +0 -0
  83. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core.egg-info/dependency_links.txt +0 -0
  84. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core.egg-info/requires.txt +0 -0
  85. {runsight_core-0.2.2 → runsight_core-0.2.3}/src/runsight_core.egg-info/top_level.txt +0 -0
  86. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_achat_budget_enforcement.py +0 -0
  87. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_artifact_store_wiring.py +0 -0
  88. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_artifacts.py +0 -0
  89. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_base_block.py +0 -0
  90. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_baseblock_artifact_helpers.py +0 -0
  91. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_block_timeout_enforcement.py +0 -0
  92. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_blocks.py +0 -0
  93. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_budget_enforcement_types.py +0 -0
  94. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_budget_limits_schema.py +0 -0
  95. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_budget_migration_remaining.py +0 -0
  96. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_budget_models.py +0 -0
  97. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_budget_session.py +0 -0
  98. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_budget_wiring.py +0 -0
  99. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_carry_context_blockresult.py +0 -0
  100. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_code_block.py +0 -0
  101. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_codeblock_sandbox_hardening.py +0 -0
  102. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_composite_observer_isolation.py +0 -0
  103. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_condition_engine.py +0 -0
  104. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_conftest_isolation_mock.py +0 -0
  105. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_context_truncation.py +0 -0
  106. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_conversation_histories.py +0 -0
  107. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_cross_feature_integration.py +0 -0
  108. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_custom_asset_tool_contract.py +0 -0
  109. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_discovery.py +0 -0
  110. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_dispatch_block_stateful.py +0 -0
  111. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_dispatch_budget_isolation.py +0 -0
  112. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_dispatch_exit_def.py +0 -0
  113. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_dispatch_synthesize_integration.py +0 -0
  114. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_dispatch_v2.py +0 -0
  115. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_e2e_block_timeout.py +0 -0
  116. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_e2e_cost_cap.py +0 -0
  117. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_e2e_dispatch_budget.py +0 -0
  118. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_e2e_warn_and_flow_timeout.py +0 -0
  119. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_fit_to_budget_phase1.py +0 -0
  120. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_fit_to_budget_phase2.py +0 -0
  121. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_gate_error_subclass.py +0 -0
  122. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_gate_file_writer_blocks.py +0 -0
  123. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_blocks_workflow.py +0 -0
  124. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_merge_validation.py +0 -0
  125. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_runner_primitives.py +0 -0
  126. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_state_blocks.py +0 -0
  127. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_workflow.py +0 -0
  128. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_workflow_block.py +0 -0
  129. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_workflow_block_backward_compat.py +0 -0
  130. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_workflow_block_e2e.py +0 -0
  131. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_workflow_block_parser.py +0 -0
  132. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_integration_workflow_block_with_other_blocks.py +0 -0
  133. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_001_envelope_models.py +0 -0
  134. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_002_ipc_protocol.py +0 -0
  135. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_003_harness.py +0 -0
  136. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_004_worker.py +0 -0
  137. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_005_block_migration.py +0 -0
  138. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_006_dispatch_delegate.py +0 -0
  139. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_007_monitoring.py +0 -0
  140. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_008_credentials.py +0 -0
  141. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_iso_e2e.py +0 -0
  142. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_linearblock_stateful.py +0 -0
  143. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loop_block.py +0 -0
  144. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loop_break_conditions.py +0 -0
  145. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loop_carry_context.py +0 -0
  146. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loop_exports_schema.py +0 -0
  147. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loop_workflow_validation.py +0 -0
  148. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loopblock_kwargs_forwarding.py +0 -0
  149. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_loopblock_stateful_integration.py +0 -0
  150. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_model_catalog.py +0 -0
  151. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_observer.py +0 -0
  152. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_observer_soul_extension.py +0 -0
  153. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_parser_inputs_outputs.py +0 -0
  154. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_parser_workflow_block.py +0 -0
  155. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_primitives.py +0 -0
  156. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_primitives_extended.py +0 -0
  157. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_prompt_hash.py +0 -0
  158. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_registry.py +0 -0
  159. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_remove_placeholder_block.py +0 -0
  160. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_retry_config.py +0 -0
  161. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_retry_execution.py +0 -0
  162. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_retry_stateful.py +0 -0
  163. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_retryblock_migration.py +0 -0
  164. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run126_code_block_parser_and_achat.py +0 -0
  165. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run127_runner_get_client_api_key.py +0 -0
  166. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run137_async_subprocess.py +0 -0
  167. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run141_multi_provider_keys.py +0 -0
  168. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run170_complex_read_sites.py +0 -0
  169. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run177_block_result.py +0 -0
  170. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run178_write_sites_block_result.py +0 -0
  171. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run179_strict_block_result.py +0 -0
  172. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run181_read_site_migration.py +0 -0
  173. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run222_migrate_blocks.py +0 -0
  174. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run377_yaml_enabled.py +0 -0
  175. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run415_no_builtin_souls.py +0 -0
  176. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run468_parser_soul_field_forwarding.py +0 -0
  177. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run469_discover_soul_fields.py +0 -0
  178. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run569_project_root_resolution.py +0 -0
  179. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run570_kill_inline_souls.py +0 -0
  180. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run571_wire_soul_ref_to_library.py +0 -0
  181. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run572_library_soul_tool_governance.py +0 -0
  182. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run603_workflow_interface_schema.py +0 -0
  183. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run604_interface_execution.py +0 -0
  184. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run605_on_error_modes.py +0 -0
  185. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run606_runtime_depth_parity.py +0 -0
  186. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run614_integration_subworkflow.py +0 -0
  187. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run628_noise_cleanup_verification.py +0 -0
  188. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run629_dispatch_e2e.py +0 -0
  189. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run644_dispatch_runtime_rename.py +0 -0
  190. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run645_dispatch_schema_canonicalization.py +0 -0
  191. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run663_child_observer_wrapper.py +0 -0
  192. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run663_parser_round_trip.py +0 -0
  193. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run668_depends_error_routes.py +0 -0
  194. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run669_gate_shortcuts.py +0 -0
  195. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run670_error_route_runtime.py +0 -0
  196. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run671_routes_shorthand.py +0 -0
  197. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run675_block_execution_context.py +0 -0
  198. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run676_execute_block_extraction.py +0 -0
  199. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run677_workflow_run_execute_block_wiring.py +0 -0
  200. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run678_loop_execute_block_wiring.py +0 -0
  201. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run680_codeblock_exit_handle.py +0 -0
  202. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run681_linearblock_exit_conditions.py +0 -0
  203. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run682_workflowblock_loopblock_e2e.py +0 -0
  204. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run683_nested_loopblock_observer.py +0 -0
  205. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run684_exit_handle_all_block_types.py +0 -0
  206. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run685_eval_debt_integration.py +0 -0
  207. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run688_soul_assertions_cleanup.py +0 -0
  208. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run690_delete_duplicate_resolve_soul.py +0 -0
  209. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run692_inline_soul_fixture_migration.py +0 -0
  210. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run693_step_wrapper_assertions.py +0 -0
  211. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run694_eval_yaml_schema.py +0 -0
  212. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run695_eval_runner.py +0 -0
  213. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run699_eval_integration.py +0 -0
  214. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run700_eval_e2e.py +0 -0
  215. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run701_state_isolation_verification.py +0 -0
  216. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run702_mixed_pipeline_e2e.py +0 -0
  217. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run703_dispatch_in_loop_e2e.py +0 -0
  218. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run704_error_route_output_mapping_e2e.py +0 -0
  219. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run787_discovery_foundation.py +0 -0
  220. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run788_workflow_repo_soul_scanner.py +0 -0
  221. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run789_tool_scanner_callers.py +0 -0
  222. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run789_tools_router_scanner.py +0 -0
  223. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run790_workflow_repo_scanner.py +0 -0
  224. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run790_workflow_scanner.py +0 -0
  225. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run792_unified_scanner_integration.py +0 -0
  226. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run794_assertion_scanner.py +0 -0
  227. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run797_custom_assertion_registration.py +0 -0
  228. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run800_custom_assertion_eval_e2e.py +0 -0
  229. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run803_tool_pydantic_validation.py +0 -0
  230. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run817_ipc_models_extract.py +0 -0
  231. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run818_interceptors_extract.py +0 -0
  232. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run819_worker_proxies_extract.py +0 -0
  233. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run820_worker_support_extract.py +0 -0
  234. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_run_821_docker_hardening.py +0 -0
  235. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_runner.py +0 -0
  236. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_runner_messages.py +0 -0
  237. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_sandbox_hardening.py +0 -0
  238. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_schema.py +0 -0
  239. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_schema_validation.py +0 -0
  240. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_state.py +0 -0
  241. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_tool_integration.py +0 -0
  242. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_tool_registry.py +0 -0
  243. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_windowing.py +0 -0
  244. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_workflow.py +0 -0
  245. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_workflow_block_execute.py +0 -0
  246. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_workflow_block_recursion.py +0 -0
  247. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_workflow_defensive_observer.py +0 -0
  248. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_workflow_output_conditions.py +0 -0
  249. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_yaml_assertions_config.py +0 -0
  250. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_yaml_dx_e2e.py +0 -0
  251. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_yaml_dx_sugar.py +0 -0
  252. {runsight_core-0.2.2 → runsight_core-0.2.3}/tests/test_yaml_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runsight-core
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Runsight Agent OS Core Engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "runsight-core"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Runsight Agent OS Core Engine"
9
9
  requires-python = ">=3.11"
10
10
  license = "Apache-2.0"
@@ -33,7 +33,16 @@ def register_block_def(block_type: str, def_cls: Type["Any"]) -> None:
33
33
 
34
34
 
35
35
  def register_block_builder(block_type: str, builder: Callable) -> None:
36
- """Register a builder callable for *block_type*."""
36
+ """Register a builder callable for *block_type*.
37
+
38
+ Raises ``ValueError`` if *block_type* is already registered with a
39
+ **different** callable (re-registering the same callable is idempotent).
40
+ """
41
+ existing = BLOCK_BUILDER_REGISTRY.get(block_type)
42
+ if existing is not None and existing is not builder:
43
+ raise ValueError(
44
+ f"Duplicate block-builder registration for '{block_type}': {existing!r} vs {builder!r}"
45
+ )
37
46
  BLOCK_BUILDER_REGISTRY[block_type] = builder
38
47
 
39
48
 
@@ -334,6 +334,7 @@ def build(
334
334
  souls_map: Dict[str, Any],
335
335
  runner: Any,
336
336
  all_blocks: Dict[str, Any],
337
+ **_: Any,
337
338
  ) -> CodeBlock:
338
339
  """Build a CodeBlock from a block definition."""
339
340
  if not block_def.code:
@@ -219,6 +219,7 @@ def build(
219
219
  souls_map: Dict[str, Any],
220
220
  runner: Any,
221
221
  all_blocks: Dict[str, Any],
222
+ **_: Any,
222
223
  ) -> DispatchBlock:
223
224
  """Build a DispatchBlock from a block definition."""
224
225
  if not block_def.exits:
@@ -169,6 +169,7 @@ def build(
169
169
  souls_map: Dict[str, Any],
170
170
  runner: Any,
171
171
  all_blocks: Dict[str, Any],
172
+ **_: Any,
172
173
  ) -> GateBlock:
173
174
  """Build a GateBlock from a block definition."""
174
175
  if block_def.soul_ref is None:
@@ -112,6 +112,7 @@ def build(
112
112
  souls_map: Dict[str, Any],
113
113
  runner: Any,
114
114
  all_blocks: Dict[str, Any],
115
+ **_: Any,
115
116
  ) -> LinearBlock:
116
117
  """Build a LinearBlock from a block definition."""
117
118
  if block_def.soul_ref is None:
@@ -294,6 +294,7 @@ def build(
294
294
  souls_map: Dict[str, Any],
295
295
  runner: Any,
296
296
  all_blocks: Dict[str, Any],
297
+ **_: Any,
297
298
  ) -> LoopBlock:
298
299
  """Build a LoopBlock from a block definition."""
299
300
  break_condition = None
@@ -119,6 +119,7 @@ def build(
119
119
  souls_map: Dict[str, Any],
120
120
  runner: Any,
121
121
  all_blocks: Dict[str, Any],
122
+ **_: Any,
122
123
  ) -> SynthesizeBlock:
123
124
  """Build a SynthesizeBlock from a block definition."""
124
125
  if block_def.soul_ref is None:
@@ -17,7 +17,7 @@ from runsight_core.state import BlockResult, WorkflowState
17
17
  if TYPE_CHECKING:
18
18
  from runsight_core.workflow import Workflow
19
19
  from runsight_core.yaml.registry import WorkflowRegistry
20
- from runsight_core.yaml.schema import WorkflowInterfaceDef
20
+ from runsight_core.yaml.schema import RunsightWorkflowFile, WorkflowInterfaceDef
21
21
 
22
22
 
23
23
  class WorkflowBlock(BaseBlock):
@@ -392,6 +392,56 @@ from runsight_core.blocks._registry import register_block_def as _register_block
392
392
  _register_block_def("workflow", WorkflowBlockDef)
393
393
 
394
394
 
395
+ def _validate_workflow_block_contract(
396
+ block_id: str,
397
+ block_def: Any,
398
+ child_file: "RunsightWorkflowFile",
399
+ ) -> None:
400
+ child_interface = child_file.interface
401
+ if child_interface is None:
402
+ raise ValueError(
403
+ f"WorkflowBlock '{block_id}': child workflow '{block_def.workflow_ref}' "
404
+ "must declare an interface"
405
+ )
406
+
407
+ declared_inputs = {item.name: item for item in child_interface.inputs}
408
+ declared_outputs = {item.name for item in child_interface.outputs}
409
+
410
+ for binding_name in (block_def.inputs or {}).keys():
411
+ if binding_name not in declared_inputs:
412
+ raise ValueError(
413
+ f"WorkflowBlock '{block_id}': unknown interface input '{binding_name}'. "
414
+ f"Declared child inputs: {sorted(declared_inputs)}"
415
+ )
416
+
417
+ missing_required = [
418
+ item.name
419
+ for item in child_interface.inputs
420
+ if item.required and item.default is None and item.name not in (block_def.inputs or {})
421
+ ]
422
+ if missing_required:
423
+ raise ValueError(
424
+ f"WorkflowBlock '{block_id}': missing required interface inputs {missing_required}"
425
+ )
426
+
427
+ for binding_name in (block_def.outputs or {}).values():
428
+ if binding_name not in declared_outputs:
429
+ raise ValueError(
430
+ f"WorkflowBlock '{block_id}': unknown interface output '{binding_name}'. "
431
+ f"Declared child outputs: {sorted(declared_outputs)}"
432
+ )
433
+
434
+
435
+ def _resolve_workflow_block_max_depth(
436
+ file_def: Any,
437
+ block_def: Any,
438
+ ) -> int:
439
+ """Resolve the max_depth value a workflow block will enforce at runtime."""
440
+ if block_def.max_depth is not None:
441
+ return block_def.max_depth
442
+ return file_def.config.get("max_workflow_depth", 10)
443
+
444
+
395
445
  # -- Builder function --------------------------------------------------------
396
446
 
397
447
 
@@ -401,17 +451,50 @@ def build(
401
451
  souls_map: Dict[str, Any],
402
452
  runner: Any,
403
453
  all_blocks: Dict[str, Any],
454
+ *,
455
+ workflow_registry: "WorkflowRegistry" | None = None,
456
+ api_keys: Dict[str, str] | None = None,
457
+ workflow_base_dir: str = ".",
458
+ parent_file_def: Any | None = None,
459
+ **_: Any,
404
460
  ) -> WorkflowBlock:
405
- """Build a WorkflowBlock from a block definition.
461
+ """Build a WorkflowBlock from a block definition."""
462
+ if getattr(block_def, "workflow_ref", None) is None:
463
+ raise ValueError(f"WorkflowBlock '{block_id}': workflow_ref is required")
464
+ if workflow_registry is None:
465
+ raise ValueError(
466
+ f"WorkflowBlock '{block_id}': workflow_registry must be provided "
467
+ "when building workflow blocks"
468
+ )
406
469
 
407
- Note: In practice, the workflow block is handled as a special case in
408
- parse_workflow_yaml because it requires a WorkflowRegistry for recursive
409
- parsing. This build() function exists for API consistency and can be
410
- used when the child_workflow is already resolved.
411
- """
412
- raise NotImplementedError(
413
- f"WorkflowBlock '{block_id}' must be built via the special-case "
414
- f"handler in parse_workflow_yaml, not via the generic builder registry."
470
+ # Import the parser lazily to keep block registration free of parser cycles.
471
+ from runsight_core.yaml.parser import parse_workflow_yaml
472
+
473
+ child_file = workflow_registry.get(block_def.workflow_ref)
474
+ _validate_workflow_block_contract(block_id, block_def, child_file)
475
+
476
+ child_raw = child_file.model_dump() if hasattr(child_file, "model_dump") else child_file
477
+ child_wf = parse_workflow_yaml(
478
+ child_raw,
479
+ workflow_registry=workflow_registry,
480
+ api_keys=api_keys,
481
+ _base_dir=workflow_base_dir,
482
+ )
483
+
484
+ max_depth = (
485
+ _resolve_workflow_block_max_depth(parent_file_def, block_def)
486
+ if parent_file_def is not None
487
+ else block_def.max_depth or 10
488
+ )
489
+ return WorkflowBlock(
490
+ block_id=block_id,
491
+ child_workflow=child_wf,
492
+ inputs=block_def.inputs or {},
493
+ outputs=block_def.outputs or {},
494
+ workflow_ref=block_def.workflow_ref,
495
+ max_depth=max_depth,
496
+ interface=child_file.interface,
497
+ on_error=block_def.on_error,
415
498
  )
416
499
 
417
500
 
@@ -386,45 +386,10 @@ from runsight_core.yaml.schema import rebuild_block_def_union as _rebuild # noq
386
386
  _rebuild()
387
387
  del _rebuild
388
388
 
389
-
390
- def _validate_workflow_block_contract(
391
- block_id: str,
392
- block_def: Any,
393
- child_file: RunsightWorkflowFile,
394
- ) -> None:
395
- child_interface = child_file.interface
396
- if child_interface is None:
397
- raise ValueError(
398
- f"WorkflowBlock '{block_id}': child workflow '{block_def.workflow_ref}' "
399
- "must declare an interface"
400
- )
401
-
402
- declared_inputs = {item.name: item for item in child_interface.inputs}
403
- declared_outputs = {item.name for item in child_interface.outputs}
404
-
405
- for binding_name in (block_def.inputs or {}).keys():
406
- if binding_name not in declared_inputs:
407
- raise ValueError(
408
- f"WorkflowBlock '{block_id}': unknown interface input '{binding_name}'. "
409
- f"Declared child inputs: {sorted(declared_inputs)}"
410
- )
411
-
412
- missing_required = [
413
- item.name
414
- for item in child_interface.inputs
415
- if item.required and item.default is None and item.name not in (block_def.inputs or {})
416
- ]
417
- if missing_required:
418
- raise ValueError(
419
- f"WorkflowBlock '{block_id}': missing required interface inputs {missing_required}"
420
- )
421
-
422
- for binding_name in (block_def.outputs or {}).values():
423
- if binding_name not in declared_outputs:
424
- raise ValueError(
425
- f"WorkflowBlock '{block_id}': unknown interface output '{binding_name}'. "
426
- f"Declared child outputs: {sorted(declared_outputs)}"
427
- )
389
+ from runsight_core.blocks.workflow_block import ( # noqa: E402
390
+ _resolve_workflow_block_max_depth,
391
+ _validate_workflow_block_contract,
392
+ )
428
393
 
429
394
 
430
395
  def _validate_workflow_block_runtime_placement(
@@ -436,16 +401,6 @@ def _validate_workflow_block_runtime_placement(
436
401
  return None
437
402
 
438
403
 
439
- def _resolve_workflow_block_max_depth(
440
- file_def: RunsightWorkflowFile,
441
- block_def: Any,
442
- ) -> int:
443
- """Resolve the max_depth value a workflow block will enforce at runtime."""
444
- if block_def.max_depth is not None:
445
- return block_def.max_depth
446
- return file_def.config.get("max_workflow_depth", 10)
447
-
448
-
449
404
  def validate_workflow_call_contracts(
450
405
  file_def: RunsightWorkflowFile,
451
406
  *,
@@ -643,53 +598,6 @@ def parse_workflow_yaml(
643
598
  # Step 5: Build all blocks (single pass)
644
599
  built_blocks: Dict[str, BaseBlock] = {}
645
600
  for block_id, block_def in file_def.blocks.items():
646
- # Special case: WorkflowBlock (type: workflow)
647
- # Handle before registry lookup to avoid "unknown type" error
648
- if block_def.type == "workflow":
649
- # Validate workflow_ref field (redundant check — already enforced by BlockDef validator)
650
- if block_def.workflow_ref is None:
651
- raise ValueError(f"WorkflowBlock '{block_id}': workflow_ref is required")
652
-
653
- # Require workflow_registry parameter
654
- if workflow_registry is None:
655
- raise ValueError(
656
- f"WorkflowBlock '{block_id}': a WorkflowRegistry must be provided "
657
- f"to parse_workflow_yaml() when workflow blocks are used. "
658
- f"Pass workflow_registry=... parameter."
659
- )
660
-
661
- # Resolve child workflow file (registry returns RunsightWorkflowFile)
662
- child_file = workflow_registry.get(block_def.workflow_ref)
663
- _validate_workflow_block_contract(block_id, block_def, child_file)
664
-
665
- # Normalize to dict so parse_workflow_yaml receives Union[str, Dict] not model instance
666
- child_raw = child_file.model_dump() if hasattr(child_file, "model_dump") else child_file
667
-
668
- # Recursively parse child workflow (passes registry + base_dir for nested workflows)
669
- child_wf = parse_workflow_yaml(
670
- child_raw,
671
- workflow_registry=workflow_registry,
672
- api_keys=api_keys,
673
- _base_dir=workflow_base_dir,
674
- )
675
-
676
- # Instantiate WorkflowBlock
677
- from runsight_core.blocks.workflow_block import WorkflowBlock
678
-
679
- block = WorkflowBlock(
680
- block_id=block_id,
681
- child_workflow=child_wf,
682
- inputs=block_def.inputs or {},
683
- outputs=block_def.outputs or {},
684
- workflow_ref=block_def.workflow_ref,
685
- max_depth=_resolve_workflow_block_max_depth(file_def, block_def),
686
- interface=child_file.interface,
687
- on_error=block_def.on_error,
688
- )
689
-
690
- built_blocks[block_id] = block
691
- continue # Skip registry lookup
692
-
693
601
  from runsight_core.blocks._registry import get_builder
694
602
 
695
603
  builder = get_builder(block_def.type)
@@ -700,7 +608,17 @@ def parse_workflow_yaml(
700
608
  f"Unknown block type '{block_def.type}' for block '{block_id}'. "
701
609
  f"Available types: {sorted(BLOCK_BUILDER_REGISTRY.keys())}"
702
610
  )
703
- built_blocks[block_id] = builder(block_id, block_def, souls_map, runner, built_blocks)
611
+ built_blocks[block_id] = builder(
612
+ block_id,
613
+ block_def,
614
+ souls_map,
615
+ runner,
616
+ built_blocks,
617
+ workflow_registry=workflow_registry,
618
+ api_keys=api_keys,
619
+ workflow_base_dir=workflow_base_dir,
620
+ parent_file_def=file_def,
621
+ )
704
622
 
705
623
  # Step 6.2: Bridge _declared_exits from schema to runtime blocks
706
624
  for block_id, block_def in file_def.blocks.items():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runsight-core
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Runsight Agent OS Core Engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -213,6 +213,7 @@ tests/test_run701_state_isolation_verification.py
213
213
  tests/test_run702_mixed_pipeline_e2e.py
214
214
  tests/test_run703_dispatch_in_loop_e2e.py
215
215
  tests/test_run704_error_route_output_mapping_e2e.py
216
+ tests/test_run774_workflow_block_builder_path.py
216
217
  tests/test_run787_discovery_foundation.py
217
218
  tests/test_run788_workflow_repo_soul_scanner.py
218
219
  tests/test_run789_tool_scanner_callers.py
@@ -101,6 +101,44 @@ class TestBlockDefRegistry:
101
101
  # Cleanup
102
102
  BLOCK_BUILDER_REGISTRY.pop("test_builder_type", None)
103
103
 
104
+ def test_register_block_builder_is_idempotent_for_same_callable(self):
105
+ """register_block_builder permits re-registering the same callable."""
106
+ from runsight_core.blocks._registry import (
107
+ BLOCK_BUILDER_REGISTRY,
108
+ register_block_builder,
109
+ )
110
+
111
+ def fake_builder():
112
+ pass
113
+
114
+ try:
115
+ register_block_builder("same_builder_type", fake_builder)
116
+ register_block_builder("same_builder_type", fake_builder)
117
+ assert BLOCK_BUILDER_REGISTRY.get("same_builder_type") is fake_builder
118
+ finally:
119
+ BLOCK_BUILDER_REGISTRY.pop("same_builder_type", None)
120
+
121
+ def test_register_block_builder_rejects_different_duplicate(self):
122
+ """register_block_builder rejects silent builder replacement."""
123
+ from runsight_core.blocks._registry import (
124
+ BLOCK_BUILDER_REGISTRY,
125
+ register_block_builder,
126
+ )
127
+
128
+ def first_builder():
129
+ pass
130
+
131
+ def second_builder():
132
+ pass
133
+
134
+ try:
135
+ register_block_builder("conflicting_builder_type", first_builder)
136
+ with pytest.raises(ValueError, match="Duplicate block-builder registration"):
137
+ register_block_builder("conflicting_builder_type", second_builder)
138
+ assert BLOCK_BUILDER_REGISTRY.get("conflicting_builder_type") is first_builder
139
+ finally:
140
+ BLOCK_BUILDER_REGISTRY.pop("conflicting_builder_type", None)
141
+
104
142
  def test_get_all_block_types_returns_dict(self):
105
143
  """get_all_block_types() returns a dict snapshot of registered types."""
106
144
  from runsight_core.blocks._registry import get_all_block_types
@@ -0,0 +1,189 @@
1
+ """
2
+ Failing tests for RUN-774: route workflow blocks through the registered builder.
3
+
4
+ These tests pin the remaining actionable debt in the workflow-block parser path:
5
+ - parse_workflow_yaml should use the registered "workflow" builder, not a parser-only special case
6
+ - the registered builder should be able to construct a real WorkflowBlock when given parser context
7
+ - missing WorkflowRegistry should fail with an actionable ValueError from the builder path
8
+ - workflow blocks referenced from LoopBlock.inner_block_refs should still come through the builder path
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from unittest.mock import MagicMock
16
+
17
+ import pytest
18
+ from runsight_core import LoopBlock, WorkflowBlock
19
+ from runsight_core.blocks._registry import BLOCK_BUILDER_REGISTRY
20
+ from runsight_core.workflow import Workflow
21
+ from runsight_core.yaml.parser import parse_workflow_yaml
22
+ from runsight_core.yaml.registry import WorkflowRegistry
23
+ from runsight_core.yaml.schema import RunsightWorkflowFile
24
+
25
+ _EMPTY_INTERFACE = {"inputs": [], "outputs": []}
26
+
27
+
28
+ def _child_workflow_file() -> RunsightWorkflowFile:
29
+ child_dict = {
30
+ "version": "1.0",
31
+ "interface": _EMPTY_INTERFACE,
32
+ "blocks": {
33
+ "child_step": {
34
+ "type": "code",
35
+ "code": "def main(data):\n return {'child_step': 'done'}",
36
+ }
37
+ },
38
+ "workflow": {
39
+ "name": "child_workflow",
40
+ "entry": "child_step",
41
+ "transitions": [{"from": "child_step", "to": None}],
42
+ },
43
+ }
44
+ return RunsightWorkflowFile.model_validate(child_dict)
45
+
46
+
47
+ def _parent_workflow_dict() -> dict[str, Any]:
48
+ return {
49
+ "version": "1.0",
50
+ "config": {
51
+ "max_workflow_depth": 7,
52
+ },
53
+ "blocks": {
54
+ "invoke_child": {
55
+ "type": "workflow",
56
+ "workflow_ref": "child_workflow",
57
+ },
58
+ "loop_block": {
59
+ "type": "loop",
60
+ "inner_block_refs": ["invoke_child"],
61
+ "max_rounds": 1,
62
+ },
63
+ },
64
+ "workflow": {
65
+ "name": "parent_workflow",
66
+ "entry": "invoke_child",
67
+ "transitions": [
68
+ {"from": "invoke_child", "to": "loop_block"},
69
+ {"from": "loop_block", "to": None},
70
+ ],
71
+ },
72
+ }
73
+
74
+
75
+ def test_parse_workflow_yaml_routes_workflow_blocks_through_registered_builder_even_inside_loops(
76
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
77
+ ) -> None:
78
+ """Parsing should invoke the registered workflow builder instead of a parser special case."""
79
+ child_file = _child_workflow_file()
80
+ registry = WorkflowRegistry(allow_filesystem_fallback=False)
81
+ registry.register("child_workflow", child_file)
82
+
83
+ calls: list[dict[str, Any]] = []
84
+
85
+ def spy_builder(
86
+ block_id: str,
87
+ block_def: Any,
88
+ souls_map: dict[str, Any],
89
+ runner: Any,
90
+ all_blocks: dict[str, Any],
91
+ **parser_context: Any,
92
+ ) -> WorkflowBlock:
93
+ calls.append(
94
+ {
95
+ "block_id": block_id,
96
+ "block_def": block_def,
97
+ "souls_map": souls_map,
98
+ "runner": runner,
99
+ "all_blocks": dict(all_blocks),
100
+ "parser_context": parser_context,
101
+ }
102
+ )
103
+ child_workflow = parse_workflow_yaml(
104
+ child_file.model_dump(),
105
+ workflow_registry=parser_context["workflow_registry"],
106
+ api_keys=parser_context.get("api_keys"),
107
+ _base_dir=parser_context["workflow_base_dir"],
108
+ )
109
+ parent_file_def = parser_context["parent_file_def"]
110
+ max_depth = (
111
+ block_def.max_depth
112
+ if block_def.max_depth is not None
113
+ else parent_file_def.config.get("max_workflow_depth", 10)
114
+ )
115
+ return WorkflowBlock(
116
+ block_id=block_id,
117
+ child_workflow=child_workflow,
118
+ inputs=block_def.inputs or {},
119
+ outputs=block_def.outputs or {},
120
+ workflow_ref=block_def.workflow_ref,
121
+ max_depth=max_depth,
122
+ interface=child_file.interface,
123
+ on_error=block_def.on_error,
124
+ )
125
+
126
+ monkeypatch.setitem(BLOCK_BUILDER_REGISTRY, "workflow", spy_builder)
127
+
128
+ parent_workflow = parse_workflow_yaml(
129
+ _parent_workflow_dict(),
130
+ workflow_registry=registry,
131
+ _base_dir=str(tmp_path),
132
+ )
133
+
134
+ assert calls, "parse_workflow_yaml did not call the registered workflow builder"
135
+ assert isinstance(parent_workflow, Workflow)
136
+ assert isinstance(parent_workflow.blocks["invoke_child"], WorkflowBlock)
137
+ assert parent_workflow.blocks["invoke_child"].child_workflow.name == "child_workflow"
138
+ assert isinstance(parent_workflow.blocks["loop_block"], LoopBlock)
139
+ assert parent_workflow.blocks["loop_block"].inner_block_refs == ["invoke_child"]
140
+
141
+
142
+ def test_registered_workflow_builder_accepts_parser_context_and_builds_real_workflow_block(
143
+ tmp_path: Path,
144
+ ) -> None:
145
+ """The registered workflow builder should build a real WorkflowBlock with parser context."""
146
+ child_file = _child_workflow_file()
147
+ registry = WorkflowRegistry(allow_filesystem_fallback=False)
148
+ registry.register("child_workflow", child_file)
149
+ parent_file = RunsightWorkflowFile.model_validate(_parent_workflow_dict())
150
+ block_def = parent_file.blocks["invoke_child"]
151
+ builder = BLOCK_BUILDER_REGISTRY["workflow"]
152
+
153
+ block = builder(
154
+ "invoke_child",
155
+ block_def,
156
+ {},
157
+ MagicMock(),
158
+ {},
159
+ workflow_registry=registry,
160
+ api_keys={},
161
+ workflow_base_dir=str(tmp_path),
162
+ parent_file_def=parent_file,
163
+ )
164
+
165
+ assert isinstance(block, WorkflowBlock)
166
+ assert block.child_workflow.name == "child_workflow"
167
+ assert block.workflow_ref == "child_workflow"
168
+ assert block.max_depth == 7
169
+
170
+
171
+ def test_registered_workflow_builder_requires_workflow_registry_with_actionable_value_error(
172
+ tmp_path: Path,
173
+ ) -> None:
174
+ """The builder should fail explicitly when workflow_registry is missing."""
175
+ parent_file = RunsightWorkflowFile.model_validate(_parent_workflow_dict())
176
+ block_def = parent_file.blocks["invoke_child"]
177
+ builder = BLOCK_BUILDER_REGISTRY["workflow"]
178
+
179
+ with pytest.raises(ValueError, match="WorkflowRegistry|workflow_registry|registry"):
180
+ builder(
181
+ "invoke_child",
182
+ block_def,
183
+ {},
184
+ MagicMock(),
185
+ {},
186
+ api_keys={},
187
+ workflow_base_dir=str(tmp_path),
188
+ parent_file_def=parent_file,
189
+ )
File without changes
File without changes