devagent-physical-engine 0.11.2__tar.gz → 0.11.4__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 (137) hide show
  1. {devagent_physical_engine-0.11.2/src/devagent_physical_engine.egg-info → devagent_physical_engine-0.11.4}/PKG-INFO +27 -5
  2. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/README.md +27 -5
  3. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/pyproject.toml +1 -1
  4. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/__init__.py +1 -1
  5. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/coordinator.py +40 -1
  6. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ai_cli.py +88 -7
  7. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/engineering_agent.py +44 -1
  8. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_adapters/openai.py +82 -2
  9. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_qualification.py +24 -0
  10. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4/src/devagent_physical_engine.egg-info}/PKG-INFO +27 -5
  11. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine.egg-info/SOURCES.txt +1 -0
  12. devagent_physical_engine-0.11.4/tests/test_ai_cli.py +100 -0
  13. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_natural_language_agent_v07.py +21 -0
  14. devagent_physical_engine-0.11.4/tests/test_openai_strict_schema_v0113.py +68 -0
  15. devagent_physical_engine-0.11.2/tests/test_ai_cli.py +0 -54
  16. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/NOTICE +0 -0
  17. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/setup.cfg +0 -0
  18. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/__init__.py +0 -0
  19. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/compiler.py +0 -0
  20. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/contracts.py +0 -0
  21. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/critic.py +0 -0
  22. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/evidence.py +0 -0
  23. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/interpreter.py +0 -0
  24. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/planner.py +0 -0
  25. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/recovery.py +0 -0
  26. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/routing.py +0 -0
  27. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/runtime.py +0 -0
  28. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/semantic.py +0 -0
  29. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/agent/structured.py +0 -0
  30. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/cli.py +0 -0
  31. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/continuous_clearance.py +0 -0
  32. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/doctor.py +0 -0
  33. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/engineering_request.py +0 -0
  34. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/execution.py +0 -0
  35. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/models.py +0 -0
  36. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/operating_envelope.py +0 -0
  37. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/__init__.py +0 -0
  38. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/benchmark.py +0 -0
  39. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/candidates.py +0 -0
  40. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/contracts.py +0 -0
  41. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/evaluator.py +0 -0
  42. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/evidence.py +0 -0
  43. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/experience.py +0 -0
  44. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/measured.py +0 -0
  45. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/optimizer.py +0 -0
  46. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/optimization/orchestrator.py +0 -0
  47. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/physical_campaign.py +0 -0
  48. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/physical_evidence.py +0 -0
  49. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/physical_motion.py +0 -0
  50. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/planning.py +0 -0
  51. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/preexecution_contract.py +0 -0
  52. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_adapters/__init__.py +0 -0
  53. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_adapters/anthropic.py +0 -0
  54. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_adapters/common.py +0 -0
  55. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_adapters/factory.py +0 -0
  56. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/provider_adapters/gemini.py +0 -0
  57. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/providers.py +0 -0
  58. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/qualification.py +0 -0
  59. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/qualification_cli.py +0 -0
  60. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/qualification_harness.py +0 -0
  61. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/robot_platform.py +0 -0
  62. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/robots.py +0 -0
  63. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/__init__.py +0 -0
  64. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/acceptance.py +0 -0
  65. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/commands.py +0 -0
  66. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/continuous_clearance_probe.py +0 -0
  67. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/doctor.py +0 -0
  68. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/fk_probe.py +0 -0
  69. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/frame_alignment.py +0 -0
  70. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/gazebo_world.py +0 -0
  71. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/joint_state_recorder.py +0 -0
  72. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/measured_motion.py +0 -0
  73. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/moveit_scene.py +0 -0
  74. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/preexecution.py +0 -0
  75. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/qualification.py +0 -0
  76. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/qualification_v10.py +0 -0
  77. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/qualification_v11.py +0 -0
  78. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/scene_probe.py +0 -0
  79. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/state_validity_probe.py +0 -0
  80. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/tf_probe.py +0 -0
  81. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/tool_scene.py +0 -0
  82. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/tool_scene_probe.py +0 -0
  83. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/trajectory.py +0 -0
  84. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/ur5e.py +0 -0
  85. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/ur5e_adapter.py +0 -0
  86. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/ur5e_v10_adapter.py +0 -0
  87. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/workpiece_scene.py +0 -0
  88. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/ros2/workpiece_scene_probe.py +0 -0
  89. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/setup_profile.py +0 -0
  90. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/sim_real_qualification.py +0 -0
  91. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/simulation.py +0 -0
  92. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/simulation_platform.py +0 -0
  93. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/tool_collision.py +0 -0
  94. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/trajectory_qualification.py +0 -0
  95. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/twin.py +0 -0
  96. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/twin_builder.py +0 -0
  97. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/twin_materialization.py +0 -0
  98. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/verification.py +0 -0
  99. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine/workpiece_lifecycle.py +0 -0
  100. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine.egg-info/dependency_links.txt +0 -0
  101. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine.egg-info/entry_points.txt +0 -0
  102. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine.egg-info/requires.txt +0 -0
  103. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/src/devagent_physical_engine.egg-info/top_level.txt +0 -0
  104. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_agent_core.py +0 -0
  105. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_agent_semantic_hardening.py +0 -0
  106. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_ai_cli_v07.py +0 -0
  107. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_continuous_clearance_v011.py +0 -0
  108. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_core.py +0 -0
  109. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_engineering_request_v07.py +0 -0
  110. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_interpreter_profiles_v08.py +0 -0
  111. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_live_provider_qualification.py +0 -0
  112. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_measured_motion_v10.py +0 -0
  113. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_optimization_v05.py +0 -0
  114. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_physical_campaign_v09.py +0 -0
  115. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_physical_evidence_v09.py +0 -0
  116. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_physical_motion_v09.py +0 -0
  117. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_preexecution_contract_v10.py +0 -0
  118. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_provider_and_qualification.py +0 -0
  119. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_qualification_cli_lifecycle_v06.py +0 -0
  120. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_qualification_harness_v06.py +0 -0
  121. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_real_provider_adapters.py +0 -0
  122. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_robot_platform_v08.py +0 -0
  123. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_ros2_production_receipts_v011.py +0 -0
  124. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_ros2_simulation.py +0 -0
  125. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_ros_trajectory_v09.py +0 -0
  126. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_setup_profile.py +0 -0
  127. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_sim_real_qualification_v011.py +0 -0
  128. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_simulation_platform_v08.py +0 -0
  129. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_tool_collision_v011.py +0 -0
  130. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_trajectory_qualification_v09.py +0 -0
  131. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_twin_materialization_v10.py +0 -0
  132. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_twin_v08.py +0 -0
  133. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_ur5e_adapter_scopes_v09.py +0 -0
  134. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_ur5e_v10_adapter_scopes.py +0 -0
  135. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_v052_stabilization.py +0 -0
  136. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_v08_catalog_mapping_regression.py +0 -0
  137. {devagent_physical_engine-0.11.2 → devagent_physical_engine-0.11.4}/tests/test_workpiece_lifecycle_v011.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devagent-physical-engine
3
- Version: 0.11.2
3
+ Version: 0.11.4
4
4
  Summary: Verification-first agentic planning runtime for robotic and industrial automation.
5
5
  Author: Tom Ha
6
6
  License: All Rights Reserved
@@ -49,10 +49,12 @@ DevAgent Smart Physical Engine adds AI-assisted engineering to existing automati
49
49
 
50
50
  ## Current version
51
51
 
52
- **v0.11.2 — Beta software / evidence-gated physical runtime**
52
+ **v0.11.4 — Beta software / evidence-gated physical runtime**
53
53
 
54
54
  The Python software stack is regression-tested on Python 3.11, 3.12, and 3.13 with separate static/coverage, package-install, and runtime dependency security gates. The UR5e Gazebo/MoveIt path now contains production-candidate continuous-clearance, tool-collision, and workpiece-lifecycle capabilities, but those physical-simulation scopes remain **EXPERIMENTAL until the executable target-workstation v0.11 campaign passes**.
55
55
 
56
+ v0.11.3 normalized provider-neutral agent schemas at the OpenAI transport boundary so strict Structured Outputs can represent DevAgent optional fields without changing Anthropic/Gemini or deterministic core semantics. v0.11.4 adds live terminal progress tracing for AI request, engineering, and provider-qualification workflows. Progress is written to `stderr`; final machine-readable JSON remains isolated on `stdout`.
57
+
56
58
  Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
57
59
 
58
60
  ## v0.11 capability status
@@ -63,6 +65,7 @@ Real robot execution remains locked. Functional-safety certification and site qu
63
65
  | OpenAI / Anthropic / Gemini structured adapters | Implemented |
64
66
  | Planner / Critic / Recovery | Implemented |
65
67
  | Natural-language engineering request front door | Implemented |
68
+ | Live terminal stage tracing (`stderr`) | Implemented; `--quiet` supported |
66
69
  | Deterministic compiler / semantic / verification gates | Implemented |
67
70
  | Multi-vendor robot profile registry | Implemented |
68
71
  | UR5e / FANUC CRX / KUKA KR / ABB IRB abstraction | Implemented; physical qualification is vendor-specific |
@@ -195,19 +198,38 @@ export ANTHROPIC_API_KEY='...'
195
198
  export GEMINI_API_KEY='...'
196
199
  ```
197
200
 
198
- Examples:
201
+ Examples use the currently supported deterministic goal action `load`:
199
202
 
200
203
  ```bash
201
204
  devagent-physical-ai doctor --provider openai
202
205
  devagent-physical-ai qualify --provider openai --model <model-id>
203
206
  devagent-physical-ai request \
204
- "Use a UR5e to move a part from conveyor_a to cnc_04" \
207
+ "Use a UR5e to load BOX_101 from conveyor_a to cnc_04. Simulate and verify the plan." \
205
208
  --provider openai --model <model-id>
206
209
  devagent-physical-ai engineer \
207
- "Use a UR5e to move a part from conveyor_a to cnc_04" \
210
+ "Use a UR5e to load BOX_101 from conveyor_a to cnc_04. Simulate and verify the plan." \
208
211
  --provider openai --model <model-id>
209
212
  ```
210
213
 
214
+ Live commands now show stage-by-stage progress while they are running, for example:
215
+
216
+ ```text
217
+ [DevAgent + 0.00s] START provider=openai model=<model-id>
218
+ [DevAgent + 0.00s] INTERPRETER understanding engineering request
219
+ [DevAgent + 6.84s] INTERPRETER validated request_id=REQ-...
220
+ [DevAgent + 6.84s] REASONING risk_score=1 max_revisions=1 critic_threshold=0.70
221
+ [DevAgent + 6.84s] PLANNER revision=0 generating high-level task graph
222
+ [DevAgent + 12.21s] PLANNER revision=0 proposal compiled and deterministically verified
223
+ [DevAgent + 12.21s] CRITIC revision=0 reviewing verified plan
224
+ [DevAgent + 15.77s] CRITIC revision=0 decision=accept score=0.91
225
+ [DevAgent + 15.77s] PLANNING verified plan accepted after revision=0
226
+ [DevAgent + 15.77s] SIMULATION running deterministic model execution
227
+ [DevAgent + 15.78s] SIMULATION deterministic model execution completed
228
+ [DevAgent + 15.78s] COMPLETE ready_for_twin_simulation=true
229
+ ```
230
+
231
+ Progress logs are emitted to `stderr`, so piping `stdout` still produces clean JSON. For CI or scripts that do not want terminal progress, add `--quiet`.
232
+
211
233
  Equivalent routing supports Anthropic and Gemini. Live AI qualification does not unlock real robot execution.
212
234
 
213
235
  ## Software production gates
@@ -8,10 +8,12 @@ DevAgent Smart Physical Engine adds AI-assisted engineering to existing automati
8
8
 
9
9
  ## Current version
10
10
 
11
- **v0.11.2 — Beta software / evidence-gated physical runtime**
11
+ **v0.11.4 — Beta software / evidence-gated physical runtime**
12
12
 
13
13
  The Python software stack is regression-tested on Python 3.11, 3.12, and 3.13 with separate static/coverage, package-install, and runtime dependency security gates. The UR5e Gazebo/MoveIt path now contains production-candidate continuous-clearance, tool-collision, and workpiece-lifecycle capabilities, but those physical-simulation scopes remain **EXPERIMENTAL until the executable target-workstation v0.11 campaign passes**.
14
14
 
15
+ v0.11.3 normalized provider-neutral agent schemas at the OpenAI transport boundary so strict Structured Outputs can represent DevAgent optional fields without changing Anthropic/Gemini or deterministic core semantics. v0.11.4 adds live terminal progress tracing for AI request, engineering, and provider-qualification workflows. Progress is written to `stderr`; final machine-readable JSON remains isolated on `stdout`.
16
+
15
17
  Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
16
18
 
17
19
  ## v0.11 capability status
@@ -22,6 +24,7 @@ Real robot execution remains locked. Functional-safety certification and site qu
22
24
  | OpenAI / Anthropic / Gemini structured adapters | Implemented |
23
25
  | Planner / Critic / Recovery | Implemented |
24
26
  | Natural-language engineering request front door | Implemented |
27
+ | Live terminal stage tracing (`stderr`) | Implemented; `--quiet` supported |
25
28
  | Deterministic compiler / semantic / verification gates | Implemented |
26
29
  | Multi-vendor robot profile registry | Implemented |
27
30
  | UR5e / FANUC CRX / KUKA KR / ABB IRB abstraction | Implemented; physical qualification is vendor-specific |
@@ -154,19 +157,38 @@ export ANTHROPIC_API_KEY='...'
154
157
  export GEMINI_API_KEY='...'
155
158
  ```
156
159
 
157
- Examples:
160
+ Examples use the currently supported deterministic goal action `load`:
158
161
 
159
162
  ```bash
160
163
  devagent-physical-ai doctor --provider openai
161
164
  devagent-physical-ai qualify --provider openai --model <model-id>
162
165
  devagent-physical-ai request \
163
- "Use a UR5e to move a part from conveyor_a to cnc_04" \
166
+ "Use a UR5e to load BOX_101 from conveyor_a to cnc_04. Simulate and verify the plan." \
164
167
  --provider openai --model <model-id>
165
168
  devagent-physical-ai engineer \
166
- "Use a UR5e to move a part from conveyor_a to cnc_04" \
169
+ "Use a UR5e to load BOX_101 from conveyor_a to cnc_04. Simulate and verify the plan." \
167
170
  --provider openai --model <model-id>
168
171
  ```
169
172
 
173
+ Live commands now show stage-by-stage progress while they are running, for example:
174
+
175
+ ```text
176
+ [DevAgent + 0.00s] START provider=openai model=<model-id>
177
+ [DevAgent + 0.00s] INTERPRETER understanding engineering request
178
+ [DevAgent + 6.84s] INTERPRETER validated request_id=REQ-...
179
+ [DevAgent + 6.84s] REASONING risk_score=1 max_revisions=1 critic_threshold=0.70
180
+ [DevAgent + 6.84s] PLANNER revision=0 generating high-level task graph
181
+ [DevAgent + 12.21s] PLANNER revision=0 proposal compiled and deterministically verified
182
+ [DevAgent + 12.21s] CRITIC revision=0 reviewing verified plan
183
+ [DevAgent + 15.77s] CRITIC revision=0 decision=accept score=0.91
184
+ [DevAgent + 15.77s] PLANNING verified plan accepted after revision=0
185
+ [DevAgent + 15.77s] SIMULATION running deterministic model execution
186
+ [DevAgent + 15.78s] SIMULATION deterministic model execution completed
187
+ [DevAgent + 15.78s] COMPLETE ready_for_twin_simulation=true
188
+ ```
189
+
190
+ Progress logs are emitted to `stderr`, so piping `stdout` still produces clean JSON. For CI or scripts that do not want terminal progress, add `--quiet`.
191
+
170
192
  Equivalent routing supports Anthropic and Gemini. Live AI qualification does not unlock real robot execution.
171
193
 
172
194
  ## Software production gates
@@ -292,4 +314,4 @@ DevAgent Smart Physical Engine
292
314
  Copyright © 2026 Tom Ha
293
315
  Original creator: Tom Ha
294
316
  Original project: https://github.com/tomha85/devagent-physical-engine
295
- All rights reserved.
317
+ All rights reserved.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devagent-physical-engine"
7
- version = "0.11.2"
7
+ version = "0.11.4"
8
8
  description = "Verification-first agentic planning runtime for robotic and industrial automation."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -41,4 +41,4 @@ __all__ = [
41
41
  "TwinValidator",
42
42
  ]
43
43
 
44
- __version__ = "0.11.2"
44
+ __version__ = "0.11.4"
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Any, Mapping
4
+ from typing import Any, Callable, Mapping
5
5
 
6
6
  from .contracts import AgentEvidence, CriticDecision, RoutingPolicy
7
7
  from .critic import CriticAgent
@@ -9,6 +9,8 @@ from .evidence import task_graph_hash
9
9
  from .planner import PlannerAgent
10
10
  from ..models import Goal, Resource, TaskGraph, WorldState
11
11
 
12
+ ProgressCallback = Callable[[str, str], None]
13
+
12
14
 
13
15
  class PlanningCoordinationError(RuntimeError):
14
16
  pass
@@ -52,12 +54,18 @@ class PlanningCoordinator:
52
54
  critic_policy: RoutingPolicy,
53
55
  *,
54
56
  engineering_context: Mapping[str, Any] | None = None,
57
+ progress: ProgressCallback | None = None,
55
58
  ) -> VerifiedPlanArtifact:
56
59
  feedback: tuple[str, ...] = ()
57
60
  planner_evidence: list[AgentEvidence] = []
58
61
  critic_evidence: list[AgentEvidence] = []
59
62
 
60
63
  for revision in range(self.max_revision_rounds + 1):
64
+ if progress is not None:
65
+ progress(
66
+ "PLANNER",
67
+ f"revision={revision} generating high-level task graph",
68
+ )
61
69
  planned = self.planner.plan(
62
70
  goal,
63
71
  resources,
@@ -67,6 +75,15 @@ class PlanningCoordinator:
67
75
  engineering_context=engineering_context,
68
76
  )
69
77
  planner_evidence.append(planned.evidence)
78
+ if progress is not None:
79
+ progress(
80
+ "PLANNER",
81
+ f"revision={revision} proposal compiled and deterministically verified",
82
+ )
83
+ progress(
84
+ "CRITIC",
85
+ f"revision={revision} reviewing verified plan",
86
+ )
70
87
 
71
88
  reviewed = self.critic.review(
72
89
  goal,
@@ -77,11 +94,24 @@ class PlanningCoordinator:
77
94
  engineering_context=engineering_context,
78
95
  )
79
96
  critic_evidence.append(reviewed.evidence)
97
+ if progress is not None:
98
+ progress(
99
+ "CRITIC",
100
+ (
101
+ f"revision={revision} decision={reviewed.critique.decision.value} "
102
+ f"score={reviewed.critique.score:.2f}"
103
+ ),
104
+ )
80
105
 
81
106
  if (
82
107
  reviewed.critique.decision is CriticDecision.ACCEPT
83
108
  and reviewed.critique.score >= self.min_accept_score
84
109
  ):
110
+ if progress is not None:
111
+ progress(
112
+ "PLANNING",
113
+ f"verified plan accepted after revision={revision}",
114
+ )
85
115
  return VerifiedPlanArtifact(
86
116
  graph=planned.graph,
87
117
  graph_hash=task_graph_hash(planned.graph),
@@ -91,6 +121,8 @@ class PlanningCoordinator:
91
121
  )
92
122
 
93
123
  if reviewed.critique.decision is CriticDecision.REJECT:
124
+ if progress is not None:
125
+ progress("PLANNING", f"critic rejected revision={revision}")
94
126
  raise PlanningCoordinationError(
95
127
  "critic_rejected_plan:"
96
128
  + ";".join(reviewed.critique.issues)
@@ -104,5 +136,12 @@ class PlanningCoordinator:
104
136
  feedback = reviewed.critique.issues or (
105
137
  "critic_requested_revision",
106
138
  )
139
+ if progress is not None:
140
+ progress(
141
+ "PLANNING",
142
+ f"revision={revision} requires another planning round",
143
+ )
107
144
 
145
+ if progress is not None:
146
+ progress("PLANNING", "revision budget exhausted")
108
147
  raise PlanningCoordinationError("revision_budget_exhausted")
@@ -4,6 +4,9 @@ import argparse
4
4
  import importlib.util
5
5
  import json
6
6
  import os
7
+ import sys
8
+ import time
9
+ from collections.abc import Callable
7
10
 
8
11
  from .agent import (
9
12
  AgentRuntime,
@@ -20,6 +23,8 @@ from .provider_adapters import PROVIDER_NAMES, create_provider
20
23
  from .provider_qualification import qualify_live_provider
21
24
  from .providers import ModelRegistry, ModelSpec, QualificationStatus
22
25
 
26
+ ProgressCallback = Callable[[str, str], None]
27
+
23
28
 
24
29
  def _add_live_request_args(parser: argparse.ArgumentParser) -> None:
25
30
  parser.add_argument("text", help="Natural-language engineering request")
@@ -33,6 +38,11 @@ def _add_live_request_args(parser: argparse.ArgumentParser) -> None:
33
38
  )
34
39
  parser.add_argument("--timeout", type=float, default=30.0)
35
40
  parser.add_argument("--max-attempts", type=int, default=2)
41
+ parser.add_argument(
42
+ "--quiet",
43
+ action="store_true",
44
+ help="Suppress terminal progress logs; final JSON remains on stdout",
45
+ )
36
46
 
37
47
 
38
48
  def _parser() -> argparse.ArgumentParser:
@@ -54,6 +64,11 @@ def _parser() -> argparse.ArgumentParser:
54
64
  qualify.add_argument("--timeout", type=float, default=30.0)
55
65
  qualify.add_argument("--max-attempts", type=int, default=2)
56
66
  qualify.add_argument("--max-revisions", type=int, default=2)
67
+ qualify.add_argument(
68
+ "--quiet",
69
+ action="store_true",
70
+ help="Suppress terminal progress logs; final JSON remains on stdout",
71
+ )
57
72
 
58
73
  request = sub.add_parser(
59
74
  "request",
@@ -93,6 +108,28 @@ def provider_doctor(provider: str) -> dict:
93
108
  }
94
109
 
95
110
 
111
+ def _terminal_progress(enabled: bool) -> ProgressCallback | None:
112
+ if not enabled:
113
+ return None
114
+ started = time.monotonic()
115
+
116
+ def emit(stage: str, message: str) -> None:
117
+ elapsed = time.monotonic() - started
118
+ label = stage.strip().upper()[:12]
119
+ print(
120
+ f"[DevAgent +{elapsed:7.2f}s] {label:<12} {message}",
121
+ file=sys.stderr,
122
+ flush=True,
123
+ )
124
+
125
+ return emit
126
+
127
+
128
+ def _emit(progress: ProgressCallback | None, stage: str, message: str) -> None:
129
+ if progress is not None:
130
+ progress(stage, message)
131
+
132
+
96
133
  def _live_runtime(provider_name: str, model: str, *, timeout: float, max_attempts: int):
97
134
  provider = create_provider(provider_name)
98
135
  registry = ModelRegistry()
@@ -123,30 +160,51 @@ def _live_runtime(provider_name: str, model: str, *, timeout: float, max_attempt
123
160
  return provider, runtime, policy
124
161
 
125
162
 
126
- def _request(args: argparse.Namespace) -> int:
163
+ def _request(args: argparse.Namespace, progress: ProgressCallback | None = None) -> int:
164
+ _emit(progress, "START", f"provider={args.provider} model={args.model}")
127
165
  _, runtime, policy = _live_runtime(
128
166
  args.provider,
129
167
  args.model,
130
168
  timeout=args.timeout,
131
169
  max_attempts=args.max_attempts,
132
170
  )
171
+ _emit(progress, "INTERPRETER", "understanding engineering request")
133
172
  interpreted = RequirementInterpreterAgent(runtime).interpret(
134
173
  args.text,
135
174
  policy,
136
175
  follow_ups=args.follow_up,
137
176
  )
177
+ if interpreted.validated is not None:
178
+ _emit(
179
+ progress,
180
+ "INTERPRETER",
181
+ f"validated request_id={interpreted.validated.request_id}",
182
+ )
183
+ elif interpreted.rejection_code:
184
+ _emit(
185
+ progress,
186
+ "INTERPRETER",
187
+ f"request rejected code={interpreted.rejection_code}",
188
+ )
189
+ else:
190
+ _emit(progress, "INTERPRETER", "more engineering information required")
191
+
138
192
  payload = interpreted.to_dict()
139
193
  payload["provider_mode"] = "simulation_only_ephemeral_route"
140
194
  payload["real_execution_allowed"] = False
141
195
  print(json.dumps(payload, indent=2))
142
196
  if interpreted.validated is not None:
197
+ _emit(progress, "COMPLETE", "request validation passed")
143
198
  return 0
144
199
  if interpreted.rejection_code:
200
+ _emit(progress, "COMPLETE", "request validation rejected")
145
201
  return 4
202
+ _emit(progress, "COMPLETE", "request requires follow-up information")
146
203
  return 3
147
204
 
148
205
 
149
- def _engineer(args: argparse.Namespace) -> int:
206
+ def _engineer(args: argparse.Namespace, progress: ProgressCallback | None = None) -> int:
207
+ _emit(progress, "START", f"provider={args.provider} model={args.model}")
150
208
  _, runtime, policy = _live_runtime(
151
209
  args.provider,
152
210
  args.model,
@@ -164,6 +222,7 @@ def _engineer(args: argparse.Namespace) -> int:
164
222
  planner_policy=policy,
165
223
  critic_policy=policy,
166
224
  follow_ups=args.follow_up,
225
+ progress=progress,
167
226
  )
168
227
  payload = result.to_dict()
169
228
  payload["provider_mode"] = "simulation_only_ephemeral_route"
@@ -171,12 +230,25 @@ def _engineer(args: argparse.Namespace) -> int:
171
230
  print(json.dumps(payload, indent=2))
172
231
  if result.ready_for_twin_simulation:
173
232
  return 0
233
+ _emit(
234
+ progress,
235
+ "COMPLETE",
236
+ (
237
+ "ready_for_twin_simulation=false "
238
+ f"failure_code={result.failure_code or 'more_information_required'}"
239
+ ),
240
+ )
174
241
  if result.interpretation.validated is None and not result.interpretation.rejection_code:
175
242
  return 3
176
243
  return 4
177
244
 
178
245
 
179
- def _natural_language_failure(exc: BaseException) -> int:
246
+ def _natural_language_failure(
247
+ exc: BaseException,
248
+ *,
249
+ progress: ProgressCallback | None = None,
250
+ ) -> int:
251
+ _emit(progress, "FAILED", f"pipeline stopped type={type(exc).__name__}")
180
252
  print(
181
253
  json.dumps(
182
254
  {
@@ -198,18 +270,21 @@ def main(argv: list[str] | None = None) -> int:
198
270
  print(json.dumps(result, indent=2))
199
271
  return 0 if result["ready_for_live_qualification"] else 1
200
272
 
273
+ progress = _terminal_progress(not getattr(args, "quiet", False))
274
+
201
275
  if args.command == "request":
202
276
  try:
203
- return _request(args)
277
+ return _request(args, progress)
204
278
  except Exception as exc:
205
- return _natural_language_failure(exc)
279
+ return _natural_language_failure(exc, progress=progress)
206
280
 
207
281
  if args.command == "engineer":
208
282
  try:
209
- return _engineer(args)
283
+ return _engineer(args, progress)
210
284
  except Exception as exc:
211
- return _natural_language_failure(exc)
285
+ return _natural_language_failure(exc, progress=progress)
212
286
 
287
+ _emit(progress, "START", f"provider={args.provider} model={args.model}")
213
288
  provider = create_provider(args.provider)
214
289
  report = qualify_live_provider(
215
290
  provider,
@@ -217,6 +292,12 @@ def main(argv: list[str] | None = None) -> int:
217
292
  timeout_s=args.timeout,
218
293
  max_attempts=args.max_attempts,
219
294
  max_revision_rounds=args.max_revisions,
295
+ progress=progress,
296
+ )
297
+ _emit(
298
+ progress,
299
+ "RESULT",
300
+ f"passed={str(report.passed).lower()} failure_code={report.failure_code or 'none'}",
220
301
  )
221
302
  print(json.dumps(report.to_dict(), indent=2))
222
303
  return 0 if report.passed else 2
@@ -4,7 +4,12 @@ from dataclasses import asdict, dataclass
4
4
  from typing import Any, Sequence
5
5
 
6
6
  from .agent.contracts import AgentEvidence, RoutingPolicy
7
- from .agent.coordinator import PlanningCoordinationError, PlanningCoordinator, VerifiedPlanArtifact
7
+ from .agent.coordinator import (
8
+ PlanningCoordinationError,
9
+ PlanningCoordinator,
10
+ ProgressCallback,
11
+ VerifiedPlanArtifact,
12
+ )
8
13
  from .agent.critic import CriticAgent
9
14
  from .agent.evidence import task_graph_to_dict
10
15
  from .agent.interpreter import InterpretedEngineeringRequest, RequirementInterpreterAgent
@@ -153,7 +158,10 @@ class SmartEngineeringAgent:
153
158
  planner_policy: RoutingPolicy,
154
159
  critic_policy: RoutingPolicy,
155
160
  follow_ups: Sequence[str] = (),
161
+ progress: ProgressCallback | None = None,
156
162
  ) -> SmartEngineeringResult:
163
+ if progress is not None:
164
+ progress("INTERPRETER", "understanding engineering request")
157
165
  interpreted = self.interpreter.interpret(
158
166
  text,
159
167
  interpreter_policy,
@@ -161,12 +169,32 @@ class SmartEngineeringAgent:
161
169
  )
162
170
  request = interpreted.validated
163
171
  if request is None:
172
+ if progress is not None:
173
+ if interpreted.rejection_code:
174
+ progress(
175
+ "INTERPRETER",
176
+ f"request rejected code={interpreted.rejection_code}",
177
+ )
178
+ else:
179
+ progress("INTERPRETER", "more engineering information required")
164
180
  return SmartEngineeringResult(
165
181
  interpretation=interpreted,
166
182
  failure_code=interpreted.rejection_code,
167
183
  )
168
184
 
185
+ if progress is not None:
186
+ progress("INTERPRETER", f"validated request_id={request.request_id}")
187
+
169
188
  strategy = self.reasoning_policy.select(request)
189
+ if progress is not None:
190
+ progress(
191
+ "REASONING",
192
+ (
193
+ f"risk_score={strategy.risk_score} "
194
+ f"max_revisions={strategy.max_revision_rounds} "
195
+ f"critic_threshold={strategy.min_critic_score:.2f}"
196
+ ),
197
+ )
170
198
  goal = request.goal()
171
199
  resources = [request.resource()]
172
200
  # Explicit nominal model state, never represented as observed site state.
@@ -191,14 +219,19 @@ class SmartEngineeringAgent:
191
219
  planner_policy,
192
220
  critic_policy,
193
221
  engineering_context=request.to_dict(),
222
+ progress=progress,
194
223
  )
195
224
  except PlanningCoordinationError as exc:
225
+ if progress is not None:
226
+ progress("PLANNING", "planning coordination failed")
196
227
  return SmartEngineeringResult(
197
228
  interpretation=interpreted,
198
229
  reasoning_strategy=strategy,
199
230
  failure_code=f"planning_failed:{str(exc).split(':', 1)[0]}",
200
231
  )
201
232
  except Exception as exc:
233
+ if progress is not None:
234
+ progress("PLANNING", f"planning failed type={type(exc).__name__}")
202
235
  # Never expose provider/internal exception text as customer evidence.
203
236
  return SmartEngineeringResult(
204
237
  interpretation=interpreted,
@@ -206,6 +239,8 @@ class SmartEngineeringAgent:
206
239
  failure_code=f"planning_failed:{type(exc).__name__}",
207
240
  )
208
241
 
242
+ if progress is not None:
243
+ progress("SIMULATION", "running deterministic model execution")
209
244
  simulation_world = WorldState(facts=set(world.facts), values=dict(world.values))
210
245
  simulation = ExecutionSupervisor().execute(
211
246
  plan.graph,
@@ -216,6 +251,11 @@ class SmartEngineeringAgent:
216
251
  )
217
252
  if not simulation.completed:
218
253
  issue_codes = sorted({issue.code for issue in simulation.issues})
254
+ if progress is not None:
255
+ progress(
256
+ "SIMULATION",
257
+ "blocked by deterministic constraints",
258
+ )
219
259
  return SmartEngineeringResult(
220
260
  interpretation=interpreted,
221
261
  reasoning_strategy=strategy,
@@ -227,6 +267,9 @@ class SmartEngineeringAgent:
227
267
  ),
228
268
  )
229
269
 
270
+ if progress is not None:
271
+ progress("SIMULATION", "deterministic model execution completed")
272
+ progress("COMPLETE", "ready_for_twin_simulation=true")
230
273
  return SmartEngineeringResult(
231
274
  interpretation=interpreted,
232
275
  reasoning_strategy=strategy,
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Callable
3
+ from typing import Any, Callable, Mapping
4
4
 
5
5
  from ..agent.contracts import ModelRequest, ModelResponse
6
6
  from ..agent.runtime import ProviderError, ProviderProtocolError, ProviderUnavailable
@@ -17,6 +17,86 @@ from .common import (
17
17
  )
18
18
 
19
19
 
20
+ def _nullable_openai_schema(value: Mapping[str, Any]) -> dict[str, Any]:
21
+ """Return an OpenAI-strict-compatible nullable schema for an optional field."""
22
+
23
+ result = dict(value)
24
+ schema_type = result.get("type")
25
+ if isinstance(schema_type, str):
26
+ if schema_type != "null":
27
+ result["type"] = [schema_type, "null"]
28
+ return result
29
+ if isinstance(schema_type, list):
30
+ types = list(schema_type)
31
+ if "null" not in types:
32
+ types.append("null")
33
+ result["type"] = types
34
+ return result
35
+
36
+ enum = result.get("enum")
37
+ if isinstance(enum, list):
38
+ values = list(enum)
39
+ if None not in values:
40
+ values.append(None)
41
+ result["enum"] = values
42
+ return result
43
+
44
+ any_of = result.get("anyOf")
45
+ if isinstance(any_of, list):
46
+ branches = list(any_of)
47
+ if not any(
48
+ isinstance(branch, Mapping) and branch.get("type") == "null"
49
+ for branch in branches
50
+ ):
51
+ branches.append({"type": "null"})
52
+ result["anyOf"] = branches
53
+ return result
54
+
55
+ return {"anyOf": [result, {"type": "null"}]}
56
+
57
+
58
+ def _openai_strict_schema(schema: Mapping[str, Any]) -> dict[str, Any]:
59
+ """Normalize a provider-neutral schema for OpenAI strict Structured Outputs.
60
+
61
+ DevAgent's core schemas intentionally permit omitted optional fields because
62
+ Anthropic, Gemini, and deterministic parsers support that representation.
63
+ OpenAI strict Structured Outputs instead requires every property of every
64
+ object to appear in ``required``. Preserve provider-neutral core semantics by
65
+ making only those originally optional properties nullable in the OpenAI
66
+ transport schema, while keeping deterministic parsing/compilation as the
67
+ final authority.
68
+ """
69
+
70
+ clean = provider_schema(schema)
71
+
72
+ def normalize(value: Any) -> Any:
73
+ if isinstance(value, list):
74
+ return [normalize(item) for item in value]
75
+ if not isinstance(value, dict):
76
+ return value
77
+
78
+ result = {key: normalize(item) for key, item in value.items()}
79
+ properties = result.get("properties")
80
+ if isinstance(properties, dict):
81
+ required_raw = result.get("required", [])
82
+ required = set(required_raw) if isinstance(required_raw, list) else set()
83
+ normalized_properties: dict[str, Any] = {}
84
+ for name, property_schema in properties.items():
85
+ if name in required or not isinstance(property_schema, Mapping):
86
+ normalized_properties[name] = property_schema
87
+ else:
88
+ normalized_properties[name] = _nullable_openai_schema(property_schema)
89
+ result["properties"] = normalized_properties
90
+ result["required"] = list(normalized_properties)
91
+ result["additionalProperties"] = False
92
+ return result
93
+
94
+ normalized = normalize(clean)
95
+ if not isinstance(normalized, dict): # pragma: no cover - schema contract is mapping
96
+ raise TypeError("openai_schema_must_be_object")
97
+ return normalized
98
+
99
+
20
100
  class OpenAIProvider:
21
101
  provider_name = "openai"
22
102
 
@@ -58,7 +138,7 @@ class OpenAIProvider:
58
138
  "type": "json_schema",
59
139
  "name": f"devagent_{request.role.value}",
60
140
  "strict": True,
61
- "schema": provider_schema(request.output_schema),
141
+ "schema": _openai_strict_schema(request.output_schema),
62
142
  }
63
143
  },
64
144
  max_output_tokens=self._max_output_tokens,