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.
- devagent_physical_engine/__init__.py +44 -0
- devagent_physical_engine/agent/__init__.py +40 -0
- devagent_physical_engine/agent/compiler.py +285 -0
- devagent_physical_engine/agent/contracts.py +129 -0
- devagent_physical_engine/agent/coordinator.py +108 -0
- devagent_physical_engine/agent/critic.py +72 -0
- devagent_physical_engine/agent/evidence.py +34 -0
- devagent_physical_engine/agent/interpreter.py +179 -0
- devagent_physical_engine/agent/planner.py +105 -0
- devagent_physical_engine/agent/recovery.py +54 -0
- devagent_physical_engine/agent/routing.py +76 -0
- devagent_physical_engine/agent/runtime.py +270 -0
- devagent_physical_engine/agent/semantic.py +304 -0
- devagent_physical_engine/agent/structured.py +423 -0
- devagent_physical_engine/ai_cli.py +226 -0
- devagent_physical_engine/cli.py +392 -0
- devagent_physical_engine/doctor.py +20 -0
- devagent_physical_engine/engineering_agent.py +243 -0
- devagent_physical_engine/engineering_request.py +630 -0
- devagent_physical_engine/execution.py +90 -0
- devagent_physical_engine/models.py +143 -0
- devagent_physical_engine/operating_envelope.py +120 -0
- devagent_physical_engine/optimization/__init__.py +50 -0
- devagent_physical_engine/optimization/benchmark.py +122 -0
- devagent_physical_engine/optimization/candidates.py +198 -0
- devagent_physical_engine/optimization/contracts.py +235 -0
- devagent_physical_engine/optimization/evaluator.py +107 -0
- devagent_physical_engine/optimization/evidence.py +53 -0
- devagent_physical_engine/optimization/experience.py +105 -0
- devagent_physical_engine/optimization/measured.py +125 -0
- devagent_physical_engine/optimization/optimizer.py +215 -0
- devagent_physical_engine/optimization/orchestrator.py +155 -0
- devagent_physical_engine/physical_campaign.py +413 -0
- devagent_physical_engine/physical_evidence.py +214 -0
- devagent_physical_engine/physical_motion.py +196 -0
- devagent_physical_engine/planning.py +80 -0
- devagent_physical_engine/preexecution_contract.py +65 -0
- devagent_physical_engine/provider_adapters/__init__.py +22 -0
- devagent_physical_engine/provider_adapters/anthropic.py +112 -0
- devagent_physical_engine/provider_adapters/common.py +187 -0
- devagent_physical_engine/provider_adapters/factory.py +20 -0
- devagent_physical_engine/provider_adapters/gemini.py +126 -0
- devagent_physical_engine/provider_adapters/openai.py +95 -0
- devagent_physical_engine/provider_qualification.py +268 -0
- devagent_physical_engine/providers.py +94 -0
- devagent_physical_engine/qualification.py +44 -0
- devagent_physical_engine/qualification_cli.py +195 -0
- devagent_physical_engine/qualification_harness.py +917 -0
- devagent_physical_engine/robot_platform.py +411 -0
- devagent_physical_engine/robots.py +76 -0
- devagent_physical_engine/ros2/__init__.py +35 -0
- devagent_physical_engine/ros2/acceptance.py +324 -0
- devagent_physical_engine/ros2/commands.py +175 -0
- devagent_physical_engine/ros2/doctor.py +116 -0
- devagent_physical_engine/ros2/fk_probe.py +83 -0
- devagent_physical_engine/ros2/frame_alignment.py +61 -0
- devagent_physical_engine/ros2/gazebo_world.py +125 -0
- devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
- devagent_physical_engine/ros2/measured_motion.py +233 -0
- devagent_physical_engine/ros2/moveit_scene.py +121 -0
- devagent_physical_engine/ros2/preexecution.py +113 -0
- devagent_physical_engine/ros2/qualification.py +81 -0
- devagent_physical_engine/ros2/qualification_v10.py +252 -0
- devagent_physical_engine/ros2/scene_probe.py +219 -0
- devagent_physical_engine/ros2/state_validity_probe.py +125 -0
- devagent_physical_engine/ros2/tf_probe.py +51 -0
- devagent_physical_engine/ros2/trajectory.py +188 -0
- devagent_physical_engine/ros2/ur5e.py +59 -0
- devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
- devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
- devagent_physical_engine/setup_profile.py +356 -0
- devagent_physical_engine/simulation.py +32 -0
- devagent_physical_engine/simulation_platform.py +269 -0
- devagent_physical_engine/trajectory_qualification.py +201 -0
- devagent_physical_engine/twin.py +939 -0
- devagent_physical_engine/twin_builder.py +309 -0
- devagent_physical_engine/twin_materialization.py +404 -0
- devagent_physical_engine/verification.py +46 -0
- devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
- devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
- devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
- devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
- devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
- 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)
|