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,270 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout
4
+ from dataclasses import dataclass
5
+ from hashlib import sha256
6
+ from time import monotonic
7
+ from typing import Any, Callable, Mapping, Protocol, TypeVar
8
+ from uuid import uuid4
9
+
10
+ from .contracts import (
11
+ AgentEvidence,
12
+ AgentRole,
13
+ ModelAttempt,
14
+ ModelRequest,
15
+ ModelResponse,
16
+ RoutingPolicy,
17
+ )
18
+ from .routing import ModelRouter, RoutingError
19
+ from .structured import (
20
+ JsonSafetyError,
21
+ StructuredOutputError,
22
+ canonical_json,
23
+ json_safe,
24
+ redact_sensitive,
25
+ )
26
+ from ..providers import ModelSpec
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ class ProviderError(RuntimeError):
32
+ code = "provider_error"
33
+
34
+
35
+ class ProviderTimeout(ProviderError):
36
+ code = "provider_timeout"
37
+
38
+
39
+ class ProviderUnavailable(ProviderError):
40
+ code = "provider_unavailable"
41
+
42
+
43
+ class ProviderProtocolError(ProviderError):
44
+ code = "provider_protocol_error"
45
+
46
+
47
+ class StructuredModelProvider(Protocol):
48
+ @property
49
+ def provider_name(self) -> str: ...
50
+
51
+ def generate(
52
+ self, request: ModelRequest, *, model: str
53
+ ) -> ModelResponse: ...
54
+
55
+
56
+ class ProviderPool:
57
+ """Registry of connected provider implementations.
58
+
59
+ The pool stores provider objects only. API keys remain the provider adapter's
60
+ responsibility and are intentionally not copied into evidence records.
61
+ """
62
+
63
+ def __init__(self) -> None:
64
+ self._providers: dict[str, StructuredModelProvider] = {}
65
+
66
+ def register(self, provider: StructuredModelProvider) -> None:
67
+ name = provider.provider_name.strip().lower()
68
+ if not name:
69
+ raise ValueError("provider_name_required")
70
+ if name in self._providers:
71
+ raise ValueError(f"duplicate_provider:{name}")
72
+ self._providers[name] = provider
73
+
74
+ def get(self, name: str) -> StructuredModelProvider:
75
+ key = name.strip().lower()
76
+ try:
77
+ return self._providers[key]
78
+ except KeyError as exc:
79
+ raise ProviderUnavailable(f"provider_not_connected:{key}") from exc
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class InvocationResult:
84
+ value: Any
85
+ evidence: AgentEvidence
86
+
87
+
88
+ class AgentRuntimeError(RuntimeError):
89
+ def __init__(self, message: str, evidence: AgentEvidence) -> None:
90
+ super().__init__(message)
91
+ self.evidence = evidence
92
+
93
+
94
+ def _hash(value: Any) -> str:
95
+ return sha256(canonical_json(value).encode("utf-8")).hexdigest()
96
+
97
+
98
+ class AgentRuntime:
99
+ """Bounded structured-output runtime with fallback and audit evidence.
100
+
101
+ Provider calls run behind a wall-clock guard. Provider adapters must also
102
+ configure their own network timeout. Error details from one provider are
103
+ never forwarded to another provider; retries receive only stable internal
104
+ error codes.
105
+ """
106
+
107
+ def __init__(self, router: ModelRouter, pool: ProviderPool) -> None:
108
+ self.router = router
109
+ self.pool = pool
110
+
111
+ @staticmethod
112
+ def _generate_with_timeout(
113
+ provider: StructuredModelProvider,
114
+ request: ModelRequest,
115
+ model: str,
116
+ ) -> ModelResponse:
117
+ executor = ThreadPoolExecutor(
118
+ max_workers=1, thread_name_prefix="devagent-provider"
119
+ )
120
+ future = executor.submit(provider.generate, request, model=model)
121
+ try:
122
+ return future.result(timeout=request.timeout_s)
123
+ except FutureTimeout as exc:
124
+ future.cancel()
125
+ raise ProviderTimeout(
126
+ f"provider_timeout_after:{request.timeout_s}s"
127
+ ) from exc
128
+ finally:
129
+ executor.shutdown(wait=False, cancel_futures=True)
130
+
131
+ def invoke(
132
+ self,
133
+ *,
134
+ role: AgentRole,
135
+ system_instruction: str,
136
+ input_payload: Mapping[str, Any],
137
+ output_schema: Mapping[str, Any],
138
+ policy: RoutingPolicy,
139
+ validator: Callable[[Mapping[str, Any]], T],
140
+ ) -> InvocationResult:
141
+ trace_id = uuid4().hex
142
+ try:
143
+ normalized_input = json_safe(redact_sensitive(input_payload))
144
+ except JsonSafetyError as exc:
145
+ evidence = AgentEvidence(trace_id, role, "invalid", None, ())
146
+ raise AgentRuntimeError(
147
+ f"agent_input_not_json_safe:{exc}", evidence
148
+ ) from exc
149
+
150
+ input_hash = _hash(normalized_input)
151
+ attempts: list[ModelAttempt] = []
152
+ feedback_code = ""
153
+
154
+ try:
155
+ candidates = self.router.candidates(role, policy)
156
+ except RoutingError as exc:
157
+ evidence = AgentEvidence(trace_id, role, input_hash, None, ())
158
+ raise AgentRuntimeError(str(exc), evidence) from exc
159
+
160
+ for attempt_index in range(policy.max_attempts):
161
+ spec: ModelSpec = candidates[attempt_index % len(candidates)]
162
+ payload = dict(normalized_input)
163
+ if feedback_code:
164
+ payload["retry_context"] = {
165
+ "attempt": attempt_index + 1,
166
+ "previous_error": feedback_code,
167
+ }
168
+
169
+ request = ModelRequest(
170
+ role=role,
171
+ system_instruction=system_instruction,
172
+ input_payload=payload,
173
+ output_schema=output_schema,
174
+ timeout_s=policy.timeout_s,
175
+ )
176
+ started = monotonic()
177
+ request_id = ""
178
+
179
+ try:
180
+ provider = self.pool.get(spec.provider)
181
+ response = self._generate_with_timeout(
182
+ provider, request, spec.model
183
+ )
184
+ if not isinstance(response, ModelResponse):
185
+ raise ProviderProtocolError(
186
+ "provider_must_return_model_response"
187
+ )
188
+
189
+ request_id = response.request_id
190
+ latency_ms = (
191
+ response.latency_ms
192
+ if response.latency_ms is not None
193
+ else (monotonic() - started) * 1000
194
+ )
195
+ if (
196
+ response.provider.strip().lower() != spec.provider
197
+ or response.model.strip() != spec.model
198
+ ):
199
+ raise ProviderProtocolError(
200
+ "response_model_identity_mismatch"
201
+ )
202
+ if not isinstance(response.payload, Mapping):
203
+ raise ProviderProtocolError(
204
+ "response_payload_must_be_object"
205
+ )
206
+
207
+ value = validator(response.payload)
208
+ output_hash = _hash(response.payload)
209
+ attempts.append(
210
+ ModelAttempt(
211
+ spec.provider,
212
+ spec.model,
213
+ "success",
214
+ latency_ms=latency_ms,
215
+ request_id=request_id,
216
+ )
217
+ )
218
+ evidence = AgentEvidence(
219
+ trace_id=trace_id,
220
+ role=role,
221
+ input_hash=input_hash,
222
+ output_hash=output_hash,
223
+ attempts=tuple(attempts),
224
+ selected_provider=spec.provider,
225
+ selected_model=spec.model,
226
+ )
227
+ return InvocationResult(value, evidence)
228
+
229
+ except (StructuredOutputError, ProviderError) as exc:
230
+ latency_ms = (monotonic() - started) * 1000
231
+ error_code = (
232
+ "structured_output_invalid"
233
+ if isinstance(exc, StructuredOutputError)
234
+ else exc.code
235
+ )
236
+ feedback_code = error_code
237
+ attempts.append(
238
+ ModelAttempt(
239
+ spec.provider,
240
+ spec.model,
241
+ "failed",
242
+ error_code,
243
+ latency_ms,
244
+ request_id,
245
+ )
246
+ )
247
+ except Exception:
248
+ latency_ms = (monotonic() - started) * 1000
249
+ feedback_code = "unexpected_provider_error"
250
+ attempts.append(
251
+ ModelAttempt(
252
+ spec.provider,
253
+ spec.model,
254
+ "failed",
255
+ feedback_code,
256
+ latency_ms,
257
+ request_id,
258
+ )
259
+ )
260
+
261
+ evidence = AgentEvidence(
262
+ trace_id=trace_id,
263
+ role=role,
264
+ input_hash=input_hash,
265
+ output_hash=None,
266
+ attempts=tuple(attempts),
267
+ )
268
+ raise AgentRuntimeError(
269
+ f"agent_attempts_exhausted:{role.value}:{feedback_code}", evidence
270
+ )
@@ -0,0 +1,304 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, defaultdict
4
+
5
+ from ..models import Capability, Goal, Resource, TaskGraph, WorldState
6
+ from ..planning import SUPPORTED_GOAL_ACTIONS
7
+ from ..verification import VerificationIssue, VerificationResult
8
+
9
+
10
+ PHYSICAL_ACTIONS = frozenset({Capability.PICK, Capability.MOVE, Capability.PLACE})
11
+ LOAD_ACTIONS = frozenset({Capability.PICK, Capability.MOVE, Capability.PLACE, Capability.WAIT})
12
+ REQUIRED_PHYSICAL_CONSTRAINTS = frozenset({"collision_free", "joint_limits"})
13
+
14
+ ALLOWED_EFFECTS: dict[Capability, frozenset[str]] = {
15
+ Capability.PICK: frozenset({"object_gripped"}),
16
+ Capability.MOVE: frozenset({"at_destination"}),
17
+ Capability.PLACE: frozenset({"object_placed", "goal_complete"}),
18
+ Capability.WAIT: frozenset(),
19
+ }
20
+
21
+ REQUIRED_EFFECTS: dict[Capability, frozenset[str]] = {
22
+ Capability.PICK: frozenset({"object_gripped"}),
23
+ Capability.MOVE: frozenset({"at_destination"}),
24
+ Capability.PLACE: frozenset({"object_placed", "goal_complete"}),
25
+ Capability.WAIT: frozenset(),
26
+ }
27
+
28
+ REQUIRED_PRECONDITIONS: dict[Capability, frozenset[str]] = {
29
+ Capability.PICK: frozenset({"robot_ready", "object_available"}),
30
+ Capability.MOVE: frozenset({"robot_ready", "object_gripped"}),
31
+ Capability.PLACE: frozenset({"robot_ready", "object_gripped", "at_destination"}),
32
+ Capability.WAIT: frozenset(),
33
+ }
34
+
35
+
36
+ class AgentPlanSemanticPolicy:
37
+ """Fail-closed semantic envelope for AI-generated V0.2 plans.
38
+
39
+ The structural verifier proves that resources and capabilities exist. This
40
+ policy additionally proves that a model cannot invent arbitrary effects,
41
+ omit baseline motion constraints, or satisfy its own ungrounded
42
+ preconditions. V0.2 intentionally supports one canonical ``load`` workflow;
43
+ broader task domains must add an explicit policy instead of weakening this
44
+ one.
45
+ """
46
+
47
+ def verify(
48
+ self,
49
+ goal: Goal,
50
+ graph: TaskGraph,
51
+ resources: list[Resource],
52
+ world: WorldState,
53
+ ) -> VerificationResult:
54
+ issues: list[VerificationIssue] = []
55
+ goal_action = goal.action.strip().lower()
56
+
57
+ if goal_action not in SUPPORTED_GOAL_ACTIONS:
58
+ issues.append(
59
+ VerificationIssue(
60
+ "unsupported_goal_action",
61
+ detail=goal_action,
62
+ )
63
+ )
64
+ return VerificationResult(False, issues)
65
+
66
+ if graph.goal_id != goal.goal_id:
67
+ issues.append(
68
+ VerificationIssue(
69
+ "goal_id_mismatch",
70
+ detail=f"expected={goal.goal_id};actual={graph.goal_id}",
71
+ )
72
+ )
73
+
74
+ try:
75
+ ordered = graph.topological_order()
76
+ except ValueError as exc:
77
+ issues.append(
78
+ VerificationIssue("invalid_task_graph", detail=str(exc))
79
+ )
80
+ return VerificationResult(False, issues)
81
+
82
+ resource_map = {resource.resource_id: resource for resource in resources}
83
+ action_counts = Counter(task.contract.action for task in ordered)
84
+
85
+ for action in (Capability.PICK, Capability.MOVE, Capability.PLACE):
86
+ if action_counts[action] != 1:
87
+ issues.append(
88
+ VerificationIssue(
89
+ "invalid_action_count",
90
+ detail=f"{action.value}:{action_counts[action]}",
91
+ )
92
+ )
93
+
94
+ children: dict[str, set[str]] = defaultdict(set)
95
+ parents: dict[str, set[str]] = defaultdict(set)
96
+ for task in ordered:
97
+ for dependency in task.depends_on:
98
+ children[dependency].add(task.task_id)
99
+ parents[task.task_id].add(dependency)
100
+
101
+ def ancestors(task_id: str) -> set[str]:
102
+ discovered: set[str] = set()
103
+ pending = list(parents.get(task_id, set()))
104
+ while pending:
105
+ current = pending.pop()
106
+ if current in discovered:
107
+ continue
108
+ discovered.add(current)
109
+ pending.extend(parents.get(current, set()))
110
+ return discovered
111
+
112
+ grounded_facts = set(world.facts)
113
+ completion_tasks: list[str] = []
114
+ physical_resource_ids: set[str] = set()
115
+ action_task_ids: dict[Capability, list[str]] = defaultdict(list)
116
+
117
+ for task in ordered:
118
+ contract = task.contract
119
+ action = contract.action
120
+ action_task_ids[action].append(task.task_id)
121
+
122
+ if action not in LOAD_ACTIONS:
123
+ issues.append(
124
+ VerificationIssue(
125
+ "action_not_allowed_for_goal",
126
+ task.task_id,
127
+ action.value,
128
+ )
129
+ )
130
+ continue
131
+
132
+ if contract.resource_id not in resource_map:
133
+ issues.append(
134
+ VerificationIssue(
135
+ "unknown_resource",
136
+ task.task_id,
137
+ contract.resource_id,
138
+ )
139
+ )
140
+
141
+ if action in PHYSICAL_ACTIONS:
142
+ physical_resource_ids.add(contract.resource_id)
143
+ constraints = set(contract.constraints)
144
+ missing_constraints = REQUIRED_PHYSICAL_CONSTRAINTS - constraints
145
+ for constraint in sorted(missing_constraints):
146
+ issues.append(
147
+ VerificationIssue(
148
+ "required_constraint_missing",
149
+ task.task_id,
150
+ constraint,
151
+ )
152
+ )
153
+
154
+ required_preconditions = REQUIRED_PRECONDITIONS.get(action, frozenset())
155
+ preconditions = set(contract.preconditions)
156
+ for precondition in sorted(required_preconditions - preconditions):
157
+ issues.append(
158
+ VerificationIssue(
159
+ "required_precondition_missing",
160
+ task.task_id,
161
+ precondition,
162
+ )
163
+ )
164
+
165
+ for precondition in contract.preconditions:
166
+ if precondition not in grounded_facts:
167
+ issues.append(
168
+ VerificationIssue(
169
+ "ungrounded_precondition",
170
+ task.task_id,
171
+ precondition,
172
+ )
173
+ )
174
+
175
+ effects = set(contract.expected_effects)
176
+ allowed_effects = ALLOWED_EFFECTS.get(action, frozenset())
177
+ required_effects = REQUIRED_EFFECTS.get(action, frozenset())
178
+
179
+ for effect in sorted(effects - allowed_effects):
180
+ issues.append(
181
+ VerificationIssue(
182
+ "effect_not_allowed",
183
+ task.task_id,
184
+ effect,
185
+ )
186
+ )
187
+ for effect in sorted(required_effects - effects):
188
+ issues.append(
189
+ VerificationIssue(
190
+ "required_effect_missing",
191
+ task.task_id,
192
+ effect,
193
+ )
194
+ )
195
+
196
+ if "goal_complete" in effects:
197
+ completion_tasks.append(task.task_id)
198
+ if action is not Capability.PLACE:
199
+ issues.append(
200
+ VerificationIssue(
201
+ "goal_complete_wrong_action",
202
+ task.task_id,
203
+ action.value,
204
+ )
205
+ )
206
+
207
+ if action is Capability.PICK:
208
+ if contract.object_id != goal.object_id:
209
+ issues.append(
210
+ VerificationIssue(
211
+ "goal_object_mismatch",
212
+ task.task_id,
213
+ str(contract.object_id),
214
+ )
215
+ )
216
+ if contract.source != goal.source:
217
+ issues.append(
218
+ VerificationIssue(
219
+ "goal_source_mismatch",
220
+ task.task_id,
221
+ str(contract.source),
222
+ )
223
+ )
224
+ elif action is Capability.MOVE:
225
+ if contract.object_id != goal.object_id:
226
+ issues.append(
227
+ VerificationIssue(
228
+ "goal_object_mismatch",
229
+ task.task_id,
230
+ str(contract.object_id),
231
+ )
232
+ )
233
+ if contract.destination != goal.destination:
234
+ issues.append(
235
+ VerificationIssue(
236
+ "goal_destination_mismatch",
237
+ task.task_id,
238
+ str(contract.destination),
239
+ )
240
+ )
241
+ elif action is Capability.PLACE:
242
+ if contract.object_id != goal.object_id:
243
+ issues.append(
244
+ VerificationIssue(
245
+ "goal_object_mismatch",
246
+ task.task_id,
247
+ str(contract.object_id),
248
+ )
249
+ )
250
+ if contract.destination != goal.destination:
251
+ issues.append(
252
+ VerificationIssue(
253
+ "goal_destination_mismatch",
254
+ task.task_id,
255
+ str(contract.destination),
256
+ )
257
+ )
258
+
259
+ grounded_facts.update(effects & allowed_effects)
260
+
261
+ if len(physical_resource_ids) != 1:
262
+ issues.append(
263
+ VerificationIssue(
264
+ "load_requires_single_physical_resource",
265
+ detail=",".join(sorted(physical_resource_ids)),
266
+ )
267
+ )
268
+
269
+ if len(completion_tasks) != 1:
270
+ issues.append(
271
+ VerificationIssue(
272
+ "invalid_goal_completion_count",
273
+ detail=str(len(completion_tasks)),
274
+ )
275
+ )
276
+ elif children.get(completion_tasks[0]):
277
+ issues.append(
278
+ VerificationIssue(
279
+ "goal_completion_not_terminal",
280
+ completion_tasks[0],
281
+ )
282
+ )
283
+
284
+ pick_ids = action_task_ids.get(Capability.PICK, [])
285
+ move_ids = action_task_ids.get(Capability.MOVE, [])
286
+ place_ids = action_task_ids.get(Capability.PLACE, [])
287
+ if len(pick_ids) == len(move_ids) == len(place_ids) == 1:
288
+ pick_id, move_id, place_id = pick_ids[0], move_ids[0], place_ids[0]
289
+ if pick_id not in ancestors(move_id):
290
+ issues.append(
291
+ VerificationIssue(
292
+ "move_not_dependent_on_pick",
293
+ move_id,
294
+ )
295
+ )
296
+ if move_id not in ancestors(place_id):
297
+ issues.append(
298
+ VerificationIssue(
299
+ "place_not_dependent_on_move",
300
+ place_id,
301
+ )
302
+ )
303
+
304
+ return VerificationResult(not issues, issues)