devagent-physical-engine 0.10.0__py3-none-any.whl

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 (84) hide show
  1. devagent_physical_engine/__init__.py +44 -0
  2. devagent_physical_engine/agent/__init__.py +40 -0
  3. devagent_physical_engine/agent/compiler.py +285 -0
  4. devagent_physical_engine/agent/contracts.py +129 -0
  5. devagent_physical_engine/agent/coordinator.py +108 -0
  6. devagent_physical_engine/agent/critic.py +72 -0
  7. devagent_physical_engine/agent/evidence.py +34 -0
  8. devagent_physical_engine/agent/interpreter.py +179 -0
  9. devagent_physical_engine/agent/planner.py +105 -0
  10. devagent_physical_engine/agent/recovery.py +54 -0
  11. devagent_physical_engine/agent/routing.py +76 -0
  12. devagent_physical_engine/agent/runtime.py +270 -0
  13. devagent_physical_engine/agent/semantic.py +304 -0
  14. devagent_physical_engine/agent/structured.py +423 -0
  15. devagent_physical_engine/ai_cli.py +226 -0
  16. devagent_physical_engine/cli.py +392 -0
  17. devagent_physical_engine/doctor.py +20 -0
  18. devagent_physical_engine/engineering_agent.py +243 -0
  19. devagent_physical_engine/engineering_request.py +630 -0
  20. devagent_physical_engine/execution.py +90 -0
  21. devagent_physical_engine/models.py +143 -0
  22. devagent_physical_engine/operating_envelope.py +120 -0
  23. devagent_physical_engine/optimization/__init__.py +50 -0
  24. devagent_physical_engine/optimization/benchmark.py +122 -0
  25. devagent_physical_engine/optimization/candidates.py +198 -0
  26. devagent_physical_engine/optimization/contracts.py +235 -0
  27. devagent_physical_engine/optimization/evaluator.py +107 -0
  28. devagent_physical_engine/optimization/evidence.py +53 -0
  29. devagent_physical_engine/optimization/experience.py +105 -0
  30. devagent_physical_engine/optimization/measured.py +125 -0
  31. devagent_physical_engine/optimization/optimizer.py +215 -0
  32. devagent_physical_engine/optimization/orchestrator.py +155 -0
  33. devagent_physical_engine/physical_campaign.py +413 -0
  34. devagent_physical_engine/physical_evidence.py +214 -0
  35. devagent_physical_engine/physical_motion.py +196 -0
  36. devagent_physical_engine/planning.py +80 -0
  37. devagent_physical_engine/preexecution_contract.py +65 -0
  38. devagent_physical_engine/provider_adapters/__init__.py +22 -0
  39. devagent_physical_engine/provider_adapters/anthropic.py +112 -0
  40. devagent_physical_engine/provider_adapters/common.py +187 -0
  41. devagent_physical_engine/provider_adapters/factory.py +20 -0
  42. devagent_physical_engine/provider_adapters/gemini.py +126 -0
  43. devagent_physical_engine/provider_adapters/openai.py +95 -0
  44. devagent_physical_engine/provider_qualification.py +268 -0
  45. devagent_physical_engine/providers.py +94 -0
  46. devagent_physical_engine/qualification.py +44 -0
  47. devagent_physical_engine/qualification_cli.py +195 -0
  48. devagent_physical_engine/qualification_harness.py +917 -0
  49. devagent_physical_engine/robot_platform.py +411 -0
  50. devagent_physical_engine/robots.py +76 -0
  51. devagent_physical_engine/ros2/__init__.py +35 -0
  52. devagent_physical_engine/ros2/acceptance.py +324 -0
  53. devagent_physical_engine/ros2/commands.py +175 -0
  54. devagent_physical_engine/ros2/doctor.py +116 -0
  55. devagent_physical_engine/ros2/fk_probe.py +83 -0
  56. devagent_physical_engine/ros2/frame_alignment.py +61 -0
  57. devagent_physical_engine/ros2/gazebo_world.py +125 -0
  58. devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
  59. devagent_physical_engine/ros2/measured_motion.py +233 -0
  60. devagent_physical_engine/ros2/moveit_scene.py +121 -0
  61. devagent_physical_engine/ros2/preexecution.py +113 -0
  62. devagent_physical_engine/ros2/qualification.py +81 -0
  63. devagent_physical_engine/ros2/qualification_v10.py +252 -0
  64. devagent_physical_engine/ros2/scene_probe.py +219 -0
  65. devagent_physical_engine/ros2/state_validity_probe.py +125 -0
  66. devagent_physical_engine/ros2/tf_probe.py +51 -0
  67. devagent_physical_engine/ros2/trajectory.py +188 -0
  68. devagent_physical_engine/ros2/ur5e.py +59 -0
  69. devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
  70. devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
  71. devagent_physical_engine/setup_profile.py +356 -0
  72. devagent_physical_engine/simulation.py +32 -0
  73. devagent_physical_engine/simulation_platform.py +269 -0
  74. devagent_physical_engine/trajectory_qualification.py +201 -0
  75. devagent_physical_engine/twin.py +939 -0
  76. devagent_physical_engine/twin_builder.py +309 -0
  77. devagent_physical_engine/twin_materialization.py +404 -0
  78. devagent_physical_engine/verification.py +46 -0
  79. devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
  80. devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
  81. devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
  82. devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
  83. devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
  84. devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,392 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+ import shlex
7
+ import sys
8
+
9
+ from .doctor import run_doctor
10
+ from .execution import ExecutionMode, ExecutionSupervisor
11
+ from .models import Goal, WorldState
12
+ from .planning import CapabilityPlanner
13
+ from .qualification import run_cross_vendor_smoke
14
+ from .qualification_cli import add_qualification_parsers, run_qualification_command
15
+ from .robots import CATALOG
16
+ from .simulation import DeterministicSimulationBackend
17
+
18
+
19
+ def _parser() -> argparse.ArgumentParser:
20
+ parser = argparse.ArgumentParser(prog="devagent-physical")
21
+ sub = parser.add_subparsers(dest="command", required=True)
22
+
23
+ sub.add_parser("doctor", help="Check core Python runtime")
24
+ sub.add_parser("qualify", help="Run deterministic cross-vendor smoke qualification")
25
+ add_qualification_parsers(sub)
26
+
27
+ setup = sub.add_parser(
28
+ "setup",
29
+ help="Configure an explicit system profile; pip installation never changes OS packages",
30
+ )
31
+ setup.add_argument("--profile", choices=("ur5e-sim",), required=True)
32
+ setup.add_argument(
33
+ "--yes",
34
+ action="store_true",
35
+ help="Approve required system package changes non-interactively",
36
+ )
37
+ setup.add_argument(
38
+ "--dry-run",
39
+ action="store_true",
40
+ help="Show checks and required packages without changing the system",
41
+ )
42
+ setup.add_argument("--json", action="store_true", help="Emit machine-readable setup result")
43
+
44
+ sim = sub.add_parser("simulate", help="Run deterministic non-ROS simulation backend")
45
+ sim.add_argument("--robot", choices=sorted(CATALOG), default="ur5e")
46
+ sim.add_argument("--object", default="P17")
47
+ sim.add_argument("--source", default="conveyor_a")
48
+ sim.add_argument("--destination", default="cnc_04")
49
+ sim.add_argument("--inject-collision", action="store_true")
50
+
51
+ ros = sub.add_parser("ros", help="ROS 2 / Gazebo integration commands")
52
+ ros.add_argument("--ros-setup", default="/opt/ros/jazzy/setup.bash")
53
+ ros.add_argument("--workspace-setup", default=None)
54
+ ros_sub = ros.add_subparsers(dest="ros_command", required=True)
55
+
56
+ ros_sub.add_parser("doctor", help="Check ROS 2 Jazzy and UR simulation dependencies")
57
+
58
+ launch = ros_sub.add_parser("launch", help="Launch official UR5e Gazebo + MoveIt visual simulation")
59
+ launch.add_argument("--world-file", default=None)
60
+ launch.add_argument("--log-dir", default="~/.devagent/simulation")
61
+ launch.add_argument("--dry-run", action="store_true")
62
+
63
+ acceptance = ros_sub.add_parser("acceptance", help="Run laptop UR5e visual simulation acceptance")
64
+ acceptance.add_argument("--log-dir", default="~/.devagent/acceptance")
65
+ acceptance.add_argument("--report", default=None)
66
+ acceptance.add_argument("--startup-timeout", type=float, default=120.0)
67
+ acceptance.add_argument("--motion-timeout", type=float, default=90.0)
68
+ acceptance.add_argument("--keep-open", action="store_true")
69
+
70
+ demo = ros_sub.add_parser(
71
+ "demo",
72
+ help="Launch Gazebo + MoveIt, move the UR5e, verify motion, and keep the visual open",
73
+ )
74
+ demo.add_argument("--log-dir", default="~/.devagent/demo")
75
+ demo.add_argument("--report", default=None)
76
+ demo.add_argument("--startup-timeout", type=float, default=120.0)
77
+ demo.add_argument("--motion-timeout", type=float, default=90.0)
78
+ demo.add_argument(
79
+ "--close-after",
80
+ action="store_true",
81
+ help="Close Gazebo after a successful demo instead of leaving it visible",
82
+ )
83
+
84
+ trajectory = ros_sub.add_parser(
85
+ "qualify-trajectory-runtime",
86
+ help="Qualify only the compiled UR5e trajectory execution/measurement scope",
87
+ )
88
+ trajectory.add_argument("--log-dir", default="~/.devagent/trajectory-qualification")
89
+ trajectory.add_argument("--report", default=None)
90
+ trajectory.add_argument("--startup-timeout", type=float, default=120.0)
91
+
92
+ return parser
93
+
94
+
95
+ def _setup_profile(args: argparse.Namespace) -> int:
96
+ from .ros2 import RosDoctor, SubprocessRosRunner
97
+ from .setup_profile import (
98
+ SubprocessSystemRunner,
99
+ UR5eSimulationSetup,
100
+ format_setup_plan,
101
+ )
102
+
103
+ def verify_ros_stack() -> bool:
104
+ try:
105
+ runner = SubprocessRosRunner(ros_setup="/opt/ros/jazzy/setup.bash")
106
+ return RosDoctor(runner, ros_setup="/opt/ros/jazzy/setup.bash").run().passed
107
+ except Exception:
108
+ return False
109
+
110
+ progress = (
111
+ None
112
+ if args.json
113
+ else lambda message: print(message, file=sys.stderr, flush=True)
114
+ )
115
+ setup = UR5eSimulationSetup(
116
+ runner=SubprocessSystemRunner(stream_mutations=not args.json),
117
+ verifier=verify_ros_stack,
118
+ progress=progress,
119
+ )
120
+ plan = setup.inspect()
121
+ if not args.json:
122
+ print(format_setup_plan(plan))
123
+
124
+ if not plan.supported:
125
+ if args.json:
126
+ print(json.dumps(plan.to_dict(), indent=2))
127
+ else:
128
+ print(
129
+ "Unsupported platform for the qualified UR5e simulation profile; "
130
+ "no system changes were made."
131
+ )
132
+ return 8
133
+
134
+ if args.dry_run:
135
+ if args.json:
136
+ print(json.dumps(plan.to_dict(), indent=2))
137
+ else:
138
+ print("No system changes were made (dry-run).")
139
+ return 0
140
+
141
+ if plan.needs_install and not args.yes:
142
+ try:
143
+ answer = input("Install required system packages now? [y/N] ").strip().lower()
144
+ except EOFError:
145
+ answer = ""
146
+ if answer not in {"y", "yes"}:
147
+ if args.json:
148
+ print(
149
+ json.dumps(
150
+ {
151
+ "profile": plan.profile,
152
+ "passed": False,
153
+ "failure_code": "setup_cancelled",
154
+ },
155
+ indent=2,
156
+ )
157
+ )
158
+ else:
159
+ print("Setup cancelled; no system changes were made.")
160
+ return 7
161
+
162
+ result = setup.install(plan)
163
+ if args.json:
164
+ print(json.dumps(result.to_dict(), indent=2))
165
+ elif result.passed:
166
+ if result.changed_system:
167
+ print("UR5e simulation profile installed and verified.")
168
+ else:
169
+ print("UR5e simulation profile already installed and verified.")
170
+ print("Next: devagent-physical ros demo")
171
+ else:
172
+ print(f"Setup failed closed: {result.failure_code or 'verification_failed'}")
173
+ return 0 if result.passed else 9
174
+
175
+
176
+ def _core_qualify() -> int:
177
+ summary = run_cross_vendor_smoke()
178
+ print(
179
+ json.dumps(
180
+ {
181
+ "passed": summary.passed,
182
+ "violations": summary.violations,
183
+ "cases": [
184
+ {
185
+ "robot": case.robot,
186
+ "passed": case.passed,
187
+ "executed_tasks": case.executed_tasks,
188
+ "violations": case.violations,
189
+ }
190
+ for case in summary.cases
191
+ ],
192
+ },
193
+ indent=2,
194
+ )
195
+ )
196
+ return 0 if summary.passed else 1
197
+
198
+
199
+ def _deterministic_simulate(args: argparse.Namespace) -> int:
200
+ resource = CATALOG[args.robot]()
201
+ goal = Goal("cli-goal", "load", args.object, args.source, args.destination)
202
+ graph = CapabilityPlanner().plan(goal, [resource])
203
+ world = WorldState(facts={"robot_ready", "object_available"})
204
+ backend = DeterministicSimulationBackend({"T2"} if args.inject_collision else None)
205
+ report = ExecutionSupervisor().execute(
206
+ graph, [resource], world, backend, ExecutionMode.SIMULATION
207
+ )
208
+ print(
209
+ json.dumps(
210
+ {
211
+ "completed": report.completed,
212
+ "executed_tasks": report.executed_tasks,
213
+ "blocked_task": report.blocked_task,
214
+ "issues": [
215
+ {"code": issue.code, "task_id": issue.task_id, "detail": issue.detail}
216
+ for issue in report.issues
217
+ ],
218
+ "world_facts": sorted(world.facts),
219
+ },
220
+ indent=2,
221
+ )
222
+ )
223
+ return 0 if report.completed else 2
224
+
225
+
226
+ def _ros_runner(args: argparse.Namespace):
227
+ from .ros2 import SubprocessRosRunner
228
+
229
+ return SubprocessRosRunner(
230
+ ros_setup=args.ros_setup,
231
+ workspace_setup=args.workspace_setup,
232
+ )
233
+
234
+
235
+ def _ros_doctor(args: argparse.Namespace) -> int:
236
+ from .ros2 import RosDoctor
237
+
238
+ report = RosDoctor(_ros_runner(args), ros_setup=args.ros_setup).run()
239
+ for check in report.checks:
240
+ label = "PASS" if check.passed else ("WARN" if not check.required else "FAIL")
241
+ print(f"{check.name}: {label} ({check.detail})")
242
+ return 0 if report.passed else 3
243
+
244
+
245
+ def _ros_launch(args: argparse.Namespace) -> int:
246
+ from datetime import datetime, timezone
247
+ from .ros2 import UR5eSimulationLauncher, official_ur5e_moveit_command
248
+
249
+ command = official_ur5e_moveit_command(world_file=args.world_file)
250
+ if args.dry_run:
251
+ print(shlex.join(command))
252
+ return 0
253
+
254
+ log_dir = Path(args.log_dir).expanduser().resolve()
255
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
256
+ log_path = log_dir / f"ur5e-visual-{timestamp}.log"
257
+ simulation = UR5eSimulationLauncher(_ros_runner(args)).launch(
258
+ log_path=log_path,
259
+ world_file=args.world_file,
260
+ )
261
+ print(f"UR5e visual simulation started (pid={simulation.process.pid})")
262
+ print(f"Log: {simulation.log_path}")
263
+ print("Gazebo + MoveIt/RViz are launched by the official ur_simulation_gz package.")
264
+ print("Press Ctrl-C to stop.")
265
+ try:
266
+ return simulation.process.wait()
267
+ except KeyboardInterrupt:
268
+ simulation.stop()
269
+ return 130
270
+
271
+
272
+ def _ros_acceptance(args: argparse.Namespace) -> int:
273
+ from .ros2 import UR5eLaptopAcceptance
274
+
275
+ report = UR5eLaptopAcceptance(
276
+ _ros_runner(args), ros_setup=args.ros_setup
277
+ ).run(
278
+ log_dir=Path(args.log_dir),
279
+ startup_timeout_s=args.startup_timeout,
280
+ motion_timeout_s=args.motion_timeout,
281
+ keep_open=args.keep_open,
282
+ )
283
+ report_path = (
284
+ Path(args.report).expanduser().resolve()
285
+ if args.report
286
+ else Path(args.log_dir).expanduser().resolve() / "latest-acceptance.json"
287
+ )
288
+ report.write_json(report_path)
289
+ print(json.dumps(report.to_dict(), indent=2))
290
+ print(f"Acceptance report: {report_path}")
291
+ return 0 if report.passed else 4
292
+
293
+
294
+ def _ros_demo(args: argparse.Namespace) -> int:
295
+ from .ros2 import UR5eLaptopAcceptance
296
+
297
+ report = UR5eLaptopAcceptance(
298
+ _ros_runner(args), ros_setup=args.ros_setup
299
+ ).run(
300
+ log_dir=Path(args.log_dir),
301
+ startup_timeout_s=args.startup_timeout,
302
+ motion_timeout_s=args.motion_timeout,
303
+ keep_open=not args.close_after,
304
+ )
305
+ report_path = (
306
+ Path(args.report).expanduser().resolve()
307
+ if args.report
308
+ else Path(args.log_dir).expanduser().resolve() / "latest-demo.json"
309
+ )
310
+ report.write_json(report_path)
311
+ print(json.dumps(report.to_dict(), indent=2))
312
+ print(f"Demo report: {report_path}")
313
+ if report.passed and not args.close_after:
314
+ print("VISUAL DEMO PASS: Gazebo + MoveIt/RViz remain open so you can inspect the moved UR5e.")
315
+ elif report.passed:
316
+ print("VISUAL DEMO PASS: UR5e motion was measured and the simulator was closed cleanly.")
317
+ return 0 if report.passed else 5
318
+
319
+
320
+ def _ros_trajectory_runtime_qualification(args: argparse.Namespace) -> int:
321
+ from .ros2.qualification import (
322
+ ur5e_trajectory_runtime_cases,
323
+ ur5e_trajectory_runtime_twin,
324
+ )
325
+ from .ros2.ur5e_adapter import UR5eGazeboAdapter
326
+ from .trajectory_qualification import TrajectoryRuntimeQualificationRunner
327
+
328
+ log_dir = Path(args.log_dir).expanduser().resolve()
329
+ twin = ur5e_trajectory_runtime_twin()
330
+ adapter = UR5eGazeboAdapter(
331
+ _ros_runner(args),
332
+ ros_setup=args.ros_setup,
333
+ log_dir=log_dir / "simulation",
334
+ startup_timeout_s=args.startup_timeout,
335
+ )
336
+ report = TrajectoryRuntimeQualificationRunner().run(
337
+ adapter=adapter,
338
+ twin=twin,
339
+ cases=ur5e_trajectory_runtime_cases(twin),
340
+ )
341
+ payload = report.to_dict()
342
+ report_path = (
343
+ Path(args.report).expanduser().resolve()
344
+ if args.report
345
+ else log_dir / "latest-trajectory-runtime.json"
346
+ )
347
+ report_path.parent.mkdir(parents=True, exist_ok=True)
348
+ report_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
349
+ print(json.dumps(payload, indent=2))
350
+ print(f"Trajectory runtime qualification report: {report_path}")
351
+ return 0 if report.promotion_candidate else 6
352
+
353
+
354
+ def main(argv: list[str] | None = None) -> int:
355
+ args = _parser().parse_args(argv)
356
+
357
+ if args.command == "doctor":
358
+ checks = run_doctor()
359
+ for check in checks:
360
+ print(f"{check.name}: {'PASS' if check.passed else 'FAIL'} ({check.detail})")
361
+ return 0 if all(check.passed for check in checks) else 1
362
+
363
+ if args.command == "qualify":
364
+ return _core_qualify()
365
+
366
+ qualification_code = run_qualification_command(args)
367
+ if qualification_code is not None:
368
+ return qualification_code
369
+
370
+ if args.command == "setup":
371
+ return _setup_profile(args)
372
+
373
+ if args.command == "simulate":
374
+ return _deterministic_simulate(args)
375
+
376
+ if args.command == "ros":
377
+ if args.ros_command == "doctor":
378
+ return _ros_doctor(args)
379
+ if args.ros_command == "launch":
380
+ return _ros_launch(args)
381
+ if args.ros_command == "acceptance":
382
+ return _ros_acceptance(args)
383
+ if args.ros_command == "demo":
384
+ return _ros_demo(args)
385
+ if args.ros_command == "qualify-trajectory-runtime":
386
+ return _ros_trajectory_runtime_qualification(args)
387
+
388
+ return 2
389
+
390
+
391
+ if __name__ == "__main__":
392
+ raise SystemExit(main())
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import platform
5
+ import sys
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class Check:
11
+ name: str
12
+ passed: bool
13
+ detail: str
14
+
15
+
16
+ def run_doctor() -> tuple[Check, ...]:
17
+ return (
18
+ Check("python", sys.version_info >= (3, 11), platform.python_version()),
19
+ Check("core_package", importlib.util.find_spec("devagent_physical_engine") is not None, "importable"),
20
+ )
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Any, Sequence
5
+
6
+ from .agent.contracts import AgentEvidence, RoutingPolicy
7
+ from .agent.coordinator import PlanningCoordinationError, PlanningCoordinator, VerifiedPlanArtifact
8
+ from .agent.critic import CriticAgent
9
+ from .agent.evidence import task_graph_to_dict
10
+ from .agent.interpreter import InterpretedEngineeringRequest, RequirementInterpreterAgent
11
+ from .agent.planner import PlannerAgent
12
+ from .engineering_request import Objective, RequestPurpose, TestPreset, ValidatedEngineeringRequest
13
+ from .execution import ExecutionMode, ExecutionSupervisor
14
+ from .models import WorldState
15
+ from .simulation import DeterministicSimulationBackend
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class AdaptiveReasoningStrategy:
20
+ risk_score: int
21
+ max_revision_rounds: int
22
+ min_critic_score: float
23
+ future_physical_candidate_target: int
24
+ reasons: tuple[str, ...]
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return asdict(self)
28
+
29
+
30
+ class AdaptiveReasoningPolicy:
31
+ """Spend AI calls where engineering risk justifies them.
32
+
33
+ v0.7 intentionally optimizes reasoning depth rather than asking an LLM for
34
+ many superficially different high-level pick/move/place graphs. Physical
35
+ candidate diversity belongs to the measured motion/simulation layer.
36
+ """
37
+
38
+ def select(self, request: ValidatedEngineeringRequest) -> AdaptiveReasoningStrategy:
39
+ score = 0
40
+ reasons: list[str] = []
41
+
42
+ if request.purpose is RequestPurpose.COMMISSIONING:
43
+ score += 3
44
+ reasons.append("commissioning_intent")
45
+ elif request.purpose is RequestPurpose.QUALIFY:
46
+ score += 2
47
+ reasons.append("qualification_intent")
48
+ elif request.purpose is RequestPurpose.SIMULATE:
49
+ score += 1
50
+ reasons.append("simulation_intent")
51
+
52
+ if request.test_preset in {TestPreset.ROBUSTNESS, TestPreset.STRESS}:
53
+ score += 2
54
+ reasons.append("high_coverage_test_preset")
55
+ if request.variation.configured:
56
+ score += 2
57
+ reasons.append("physical_variation_requested")
58
+ if request.faults:
59
+ score += 2
60
+ reasons.append("fault_injection_requested")
61
+ if request.minimum_clearance_mm is not None or request.max_cycle_time_s is not None:
62
+ score += 1
63
+ reasons.append("explicit_engineering_threshold")
64
+ if request.objective is not Objective.BALANCED:
65
+ score += 1
66
+ reasons.append("explicit_optimization_objective")
67
+
68
+ if score >= 7:
69
+ return AdaptiveReasoningStrategy(score, 3, 0.85, 7, tuple(reasons))
70
+ if score >= 4:
71
+ return AdaptiveReasoningStrategy(score, 2, 0.78, 5, tuple(reasons))
72
+ return AdaptiveReasoningStrategy(score, 1, 0.70, 3, tuple(reasons))
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class SmartEngineeringResult:
77
+ interpretation: InterpretedEngineeringRequest
78
+ reasoning_strategy: AdaptiveReasoningStrategy | None = None
79
+ plan: VerifiedPlanArtifact | None = None
80
+ deterministic_simulation_completed: bool = False
81
+ failure_code: str | None = None
82
+
83
+ @property
84
+ def ready_for_twin_simulation(self) -> bool:
85
+ return (
86
+ self.interpretation.validated is not None
87
+ and self.plan is not None
88
+ and self.deterministic_simulation_completed
89
+ and self.failure_code is None
90
+ )
91
+
92
+ def to_dict(self) -> dict[str, Any]:
93
+ payload: dict[str, Any] = {
94
+ "interpretation": self.interpretation.to_dict(),
95
+ "reasoning_strategy": (
96
+ self.reasoning_strategy.to_dict()
97
+ if self.reasoning_strategy is not None
98
+ else None
99
+ ),
100
+ "deterministic_simulation_completed": self.deterministic_simulation_completed,
101
+ "physical_qualification": False,
102
+ "ready_for_twin_simulation": self.ready_for_twin_simulation,
103
+ "failure_code": self.failure_code,
104
+ "next_stage": (
105
+ "validated_twin_and_measured_physics_simulation"
106
+ if self.ready_for_twin_simulation
107
+ else None
108
+ ),
109
+ }
110
+ if self.plan is not None:
111
+ payload["plan"] = {
112
+ "graph_hash": self.plan.graph_hash,
113
+ "graph": task_graph_to_dict(self.plan.graph),
114
+ "revision_rounds": self.plan.revision_rounds,
115
+ "planner_calls": len(self.plan.planner_evidence),
116
+ "critic_calls": len(self.plan.critic_evidence),
117
+ "selection_claim": (
118
+ "verified_high_level_plan_only; not yet a measured optimized motion plan"
119
+ ),
120
+ }
121
+ else:
122
+ payload["plan"] = None
123
+ return payload
124
+
125
+
126
+ class SmartEngineeringAgent:
127
+ """Natural language -> verified high-level plan -> deterministic model smoke.
128
+
129
+ This is deliberately not a physical commissioning claim. The result becomes
130
+ eligible for the next Twin/ROS stage only after interpretation, deterministic
131
+ compilation, planner/critic verification, and deterministic model execution
132
+ all succeed.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ interpreter: RequirementInterpreterAgent,
138
+ planner: PlannerAgent,
139
+ critic: CriticAgent,
140
+ *,
141
+ reasoning_policy: AdaptiveReasoningPolicy | None = None,
142
+ ) -> None:
143
+ self.interpreter = interpreter
144
+ self.planner = planner
145
+ self.critic = critic
146
+ self.reasoning_policy = reasoning_policy or AdaptiveReasoningPolicy()
147
+
148
+ def run(
149
+ self,
150
+ text: str,
151
+ *,
152
+ interpreter_policy: RoutingPolicy,
153
+ planner_policy: RoutingPolicy,
154
+ critic_policy: RoutingPolicy,
155
+ follow_ups: Sequence[str] = (),
156
+ ) -> SmartEngineeringResult:
157
+ interpreted = self.interpreter.interpret(
158
+ text,
159
+ interpreter_policy,
160
+ follow_ups=follow_ups,
161
+ )
162
+ request = interpreted.validated
163
+ if request is None:
164
+ return SmartEngineeringResult(
165
+ interpretation=interpreted,
166
+ failure_code=interpreted.rejection_code,
167
+ )
168
+
169
+ strategy = self.reasoning_policy.select(request)
170
+ goal = request.goal()
171
+ resources = [request.resource()]
172
+ # Explicit nominal model state, never represented as observed site state.
173
+ world = WorldState(
174
+ facts={"robot_ready", "object_available"},
175
+ values={
176
+ "state_origin": "devagent_nominal_planning_model",
177
+ "request_id": request.request_id,
178
+ },
179
+ )
180
+ coordinator = PlanningCoordinator(
181
+ self.planner,
182
+ self.critic,
183
+ max_revision_rounds=strategy.max_revision_rounds,
184
+ min_accept_score=strategy.min_critic_score,
185
+ )
186
+ try:
187
+ plan = coordinator.create_verified_plan(
188
+ goal,
189
+ resources,
190
+ world,
191
+ planner_policy,
192
+ critic_policy,
193
+ engineering_context=request.to_dict(),
194
+ )
195
+ except PlanningCoordinationError as exc:
196
+ return SmartEngineeringResult(
197
+ interpretation=interpreted,
198
+ reasoning_strategy=strategy,
199
+ failure_code=f"planning_failed:{str(exc).split(':', 1)[0]}",
200
+ )
201
+ except Exception as exc:
202
+ # Never expose provider/internal exception text as customer evidence.
203
+ return SmartEngineeringResult(
204
+ interpretation=interpreted,
205
+ reasoning_strategy=strategy,
206
+ failure_code=f"planning_failed:{type(exc).__name__}",
207
+ )
208
+
209
+ simulation_world = WorldState(facts=set(world.facts), values=dict(world.values))
210
+ simulation = ExecutionSupervisor().execute(
211
+ plan.graph,
212
+ resources,
213
+ simulation_world,
214
+ DeterministicSimulationBackend(),
215
+ ExecutionMode.SIMULATION,
216
+ )
217
+ if not simulation.completed:
218
+ issue_codes = sorted({issue.code for issue in simulation.issues})
219
+ return SmartEngineeringResult(
220
+ interpretation=interpreted,
221
+ reasoning_strategy=strategy,
222
+ plan=plan,
223
+ deterministic_simulation_completed=False,
224
+ failure_code=(
225
+ "deterministic_simulation_failed:"
226
+ + (",".join(issue_codes) if issue_codes else "unknown")
227
+ ),
228
+ )
229
+
230
+ return SmartEngineeringResult(
231
+ interpretation=interpreted,
232
+ reasoning_strategy=strategy,
233
+ plan=plan,
234
+ deterministic_simulation_completed=True,
235
+ )
236
+
237
+
238
+ def successful_agent_evidence(result: SmartEngineeringResult) -> tuple[AgentEvidence, ...]:
239
+ items: list[AgentEvidence] = [result.interpretation.evidence]
240
+ if result.plan is not None:
241
+ items.extend(result.plan.planner_evidence)
242
+ items.extend(result.plan.critic_evidence)
243
+ return tuple(items)