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,44 @@
|
|
|
1
|
+
"""DevAgent Smart Physical Engine public contracts."""
|
|
2
|
+
|
|
3
|
+
from .models import (
|
|
4
|
+
ActionContract,
|
|
5
|
+
Capability,
|
|
6
|
+
Goal,
|
|
7
|
+
Resource,
|
|
8
|
+
TaskGraph,
|
|
9
|
+
TaskNode,
|
|
10
|
+
WorldState,
|
|
11
|
+
)
|
|
12
|
+
from .physical_motion import JointTrajectoryPoint, MotionExecutionMetrics, PhysicalMotionPlan
|
|
13
|
+
from .robot_platform import (
|
|
14
|
+
BUILTIN_ROBOT_PROFILES,
|
|
15
|
+
QualificationState,
|
|
16
|
+
RobotProfile,
|
|
17
|
+
RobotProfileRegistry,
|
|
18
|
+
SimulationTier,
|
|
19
|
+
)
|
|
20
|
+
from .twin import TwinConfidenceLevel, TwinSpec, TwinValidationReport, TwinValidator
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ActionContract",
|
|
24
|
+
"Capability",
|
|
25
|
+
"Goal",
|
|
26
|
+
"Resource",
|
|
27
|
+
"TaskGraph",
|
|
28
|
+
"TaskNode",
|
|
29
|
+
"WorldState",
|
|
30
|
+
"JointTrajectoryPoint",
|
|
31
|
+
"MotionExecutionMetrics",
|
|
32
|
+
"PhysicalMotionPlan",
|
|
33
|
+
"BUILTIN_ROBOT_PROFILES",
|
|
34
|
+
"QualificationState",
|
|
35
|
+
"RobotProfile",
|
|
36
|
+
"RobotProfileRegistry",
|
|
37
|
+
"SimulationTier",
|
|
38
|
+
"TwinConfidenceLevel",
|
|
39
|
+
"TwinSpec",
|
|
40
|
+
"TwinValidationReport",
|
|
41
|
+
"TwinValidator",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
__version__ = "0.10.0"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .compiler import AGENT_PLAN_SCHEMA, AgentPlanCompiler, AgentPlanProposal, AgentTaskProposal
|
|
2
|
+
from .contracts import (
|
|
3
|
+
AgentRole,
|
|
4
|
+
CriticDecision,
|
|
5
|
+
RecoveryDecision,
|
|
6
|
+
RoutingPolicy,
|
|
7
|
+
RoutingStrategy,
|
|
8
|
+
)
|
|
9
|
+
from .coordinator import PlanningCoordinator, VerifiedPlanArtifact
|
|
10
|
+
from .critic import CriticAgent
|
|
11
|
+
from .interpreter import InterpretedEngineeringRequest, RequirementInterpreterAgent
|
|
12
|
+
from .planner import PlannerAgent
|
|
13
|
+
from .recovery import RecoveryAgent
|
|
14
|
+
from .routing import ModelRouter
|
|
15
|
+
from .runtime import AgentRuntime, ProviderPool, StructuredModelProvider
|
|
16
|
+
from .semantic import AgentPlanSemanticPolicy
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AGENT_PLAN_SCHEMA",
|
|
20
|
+
"AgentPlanCompiler",
|
|
21
|
+
"AgentPlanProposal",
|
|
22
|
+
"AgentTaskProposal",
|
|
23
|
+
"AgentPlanSemanticPolicy",
|
|
24
|
+
"AgentRole",
|
|
25
|
+
"CriticDecision",
|
|
26
|
+
"RecoveryDecision",
|
|
27
|
+
"RoutingPolicy",
|
|
28
|
+
"RoutingStrategy",
|
|
29
|
+
"PlanningCoordinator",
|
|
30
|
+
"VerifiedPlanArtifact",
|
|
31
|
+
"CriticAgent",
|
|
32
|
+
"InterpretedEngineeringRequest",
|
|
33
|
+
"RequirementInterpreterAgent",
|
|
34
|
+
"PlannerAgent",
|
|
35
|
+
"RecoveryAgent",
|
|
36
|
+
"ModelRouter",
|
|
37
|
+
"AgentRuntime",
|
|
38
|
+
"ProviderPool",
|
|
39
|
+
"StructuredModelProvider",
|
|
40
|
+
]
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from .semantic import AgentPlanSemanticPolicy
|
|
8
|
+
from .structured import MAX_ITEMS, MAX_TASKS, MAX_TEXT, StructuredOutputError
|
|
9
|
+
from ..models import ActionContract, Capability, Goal, Resource, TaskGraph, TaskNode, WorldState
|
|
10
|
+
from ..planning import PlanningError, SUPPORTED_GOAL_ACTIONS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
AGENT_PLAN_SCHEMA = {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": False,
|
|
16
|
+
"required": ["goal_id", "tasks"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"goal_id": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
|
|
19
|
+
"tasks": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"minItems": 1,
|
|
22
|
+
"maxItems": MAX_TASKS,
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": False,
|
|
26
|
+
"required": ["task_id", "action", "resource_id"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"task_id": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
|
|
29
|
+
"action": {"enum": ["pick", "move", "place"]},
|
|
30
|
+
"resource_id": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
|
|
31
|
+
"object_id": {"type": ["string", "null"], "maxLength": MAX_TEXT},
|
|
32
|
+
"source": {"type": ["string", "null"], "maxLength": MAX_TEXT},
|
|
33
|
+
"destination": {"type": ["string", "null"], "maxLength": MAX_TEXT},
|
|
34
|
+
"depends_on": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"maxItems": MAX_ITEMS,
|
|
37
|
+
"items": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
|
|
38
|
+
"uniqueItems": True,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class AgentTaskProposal:
|
|
49
|
+
task_id: str
|
|
50
|
+
action: Capability
|
|
51
|
+
resource_id: str
|
|
52
|
+
object_id: str | None = None
|
|
53
|
+
source: str | None = None
|
|
54
|
+
destination: str | None = None
|
|
55
|
+
depends_on: tuple[str, ...] = ()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class AgentPlanProposal:
|
|
60
|
+
goal_id: str
|
|
61
|
+
tasks: tuple[AgentTaskProposal, ...]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _text(value: Any, name: str, *, required: bool = True) -> str | None:
|
|
65
|
+
if value is None and not required:
|
|
66
|
+
return None
|
|
67
|
+
if not isinstance(value, str):
|
|
68
|
+
raise StructuredOutputError(f"{name}_must_be_string")
|
|
69
|
+
normalized = value.strip()
|
|
70
|
+
if required and not normalized:
|
|
71
|
+
raise StructuredOutputError(f"{name}_required")
|
|
72
|
+
if len(normalized) > MAX_TEXT:
|
|
73
|
+
raise StructuredOutputError(f"{name}_too_long")
|
|
74
|
+
return normalized
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dependencies(value: Any, name: str) -> tuple[str, ...]:
|
|
78
|
+
if value is None:
|
|
79
|
+
return ()
|
|
80
|
+
if not isinstance(value, list):
|
|
81
|
+
raise StructuredOutputError(f"{name}_must_be_array")
|
|
82
|
+
if len(value) > MAX_ITEMS:
|
|
83
|
+
raise StructuredOutputError(f"{name}_too_many_items")
|
|
84
|
+
result: list[str] = []
|
|
85
|
+
for item in value:
|
|
86
|
+
normalized = _text(item, name)
|
|
87
|
+
if normalized is None:
|
|
88
|
+
raise StructuredOutputError(f"{name}_required")
|
|
89
|
+
result.append(normalized)
|
|
90
|
+
if len(result) != len(set(result)):
|
|
91
|
+
raise StructuredOutputError(f"{name}_contains_duplicates")
|
|
92
|
+
return tuple(result)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_agent_plan(
|
|
96
|
+
payload: Mapping[str, Any], *, expected_goal_id: str
|
|
97
|
+
) -> AgentPlanProposal:
|
|
98
|
+
if not isinstance(payload, Mapping):
|
|
99
|
+
raise StructuredOutputError("agent_plan_must_be_object")
|
|
100
|
+
extra = set(payload) - {"goal_id", "tasks"}
|
|
101
|
+
if extra:
|
|
102
|
+
raise StructuredOutputError(
|
|
103
|
+
"agent_plan_unknown_fields:" + ",".join(sorted(extra))
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
goal_id = _text(payload.get("goal_id"), "goal_id")
|
|
107
|
+
if goal_id != expected_goal_id:
|
|
108
|
+
raise StructuredOutputError("goal_id_mismatch")
|
|
109
|
+
|
|
110
|
+
raw_tasks = payload.get("tasks")
|
|
111
|
+
if not isinstance(raw_tasks, list):
|
|
112
|
+
raise StructuredOutputError("tasks_must_be_array")
|
|
113
|
+
if not raw_tasks:
|
|
114
|
+
raise StructuredOutputError("tasks_required")
|
|
115
|
+
if len(raw_tasks) > MAX_TASKS:
|
|
116
|
+
raise StructuredOutputError("too_many_tasks")
|
|
117
|
+
|
|
118
|
+
allowed = {
|
|
119
|
+
"task_id",
|
|
120
|
+
"action",
|
|
121
|
+
"resource_id",
|
|
122
|
+
"object_id",
|
|
123
|
+
"source",
|
|
124
|
+
"destination",
|
|
125
|
+
"depends_on",
|
|
126
|
+
}
|
|
127
|
+
tasks: list[AgentTaskProposal] = []
|
|
128
|
+
for index, raw in enumerate(raw_tasks):
|
|
129
|
+
if not isinstance(raw, Mapping):
|
|
130
|
+
raise StructuredOutputError(f"task_{index}_must_be_object")
|
|
131
|
+
extra = set(raw) - allowed
|
|
132
|
+
if extra:
|
|
133
|
+
raise StructuredOutputError(
|
|
134
|
+
f"task_{index}_unknown_fields:" + ",".join(sorted(extra))
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
task_id = _text(raw.get("task_id"), f"task_{index}.task_id")
|
|
138
|
+
resource_id = _text(
|
|
139
|
+
raw.get("resource_id"), f"task_{index}.resource_id"
|
|
140
|
+
)
|
|
141
|
+
action_text = _text(raw.get("action"), f"task_{index}.action")
|
|
142
|
+
if task_id is None or resource_id is None or action_text is None:
|
|
143
|
+
raise StructuredOutputError(f"task_{index}_required_field_missing")
|
|
144
|
+
try:
|
|
145
|
+
action = Capability(action_text)
|
|
146
|
+
except ValueError as exc:
|
|
147
|
+
raise StructuredOutputError(
|
|
148
|
+
f"task_{index}.unsupported_action:{action_text}"
|
|
149
|
+
) from exc
|
|
150
|
+
if action not in {Capability.PICK, Capability.MOVE, Capability.PLACE}:
|
|
151
|
+
raise StructuredOutputError(
|
|
152
|
+
f"task_{index}.action_not_allowed_for_load:{action.value}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
tasks.append(
|
|
156
|
+
AgentTaskProposal(
|
|
157
|
+
task_id=task_id,
|
|
158
|
+
action=action,
|
|
159
|
+
resource_id=resource_id,
|
|
160
|
+
object_id=_text(
|
|
161
|
+
raw.get("object_id"),
|
|
162
|
+
f"task_{index}.object_id",
|
|
163
|
+
required=False,
|
|
164
|
+
),
|
|
165
|
+
source=_text(
|
|
166
|
+
raw.get("source"),
|
|
167
|
+
f"task_{index}.source",
|
|
168
|
+
required=False,
|
|
169
|
+
),
|
|
170
|
+
destination=_text(
|
|
171
|
+
raw.get("destination"),
|
|
172
|
+
f"task_{index}.destination",
|
|
173
|
+
required=False,
|
|
174
|
+
),
|
|
175
|
+
depends_on=_dependencies(
|
|
176
|
+
raw.get("depends_on"), f"task_{index}.depends_on"
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
ids = [task.task_id for task in tasks]
|
|
182
|
+
if len(ids) != len(set(ids)):
|
|
183
|
+
raise StructuredOutputError("duplicate_task_id")
|
|
184
|
+
known = set(ids)
|
|
185
|
+
for task in tasks:
|
|
186
|
+
for dependency in task.depends_on:
|
|
187
|
+
if dependency not in known:
|
|
188
|
+
raise StructuredOutputError(
|
|
189
|
+
f"unknown_dependency:{task.task_id}:{dependency}"
|
|
190
|
+
)
|
|
191
|
+
if dependency == task.task_id:
|
|
192
|
+
raise StructuredOutputError(
|
|
193
|
+
f"self_dependency:{task.task_id}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return AgentPlanProposal(goal_id=goal_id, tasks=tuple(tasks))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class AgentPlanCompiler:
|
|
200
|
+
"""Compile an untrusted high-level proposal into canonical action contracts.
|
|
201
|
+
|
|
202
|
+
The model chooses task decomposition and resource assignment. The engine owns
|
|
203
|
+
preconditions, effects, motion constraints, and completion semantics.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self, semantic_policy: AgentPlanSemanticPolicy | None = None
|
|
208
|
+
) -> None:
|
|
209
|
+
self.semantic_policy = semantic_policy or AgentPlanSemanticPolicy()
|
|
210
|
+
|
|
211
|
+
def compile(
|
|
212
|
+
self,
|
|
213
|
+
payload: Mapping[str, Any],
|
|
214
|
+
*,
|
|
215
|
+
goal: Goal,
|
|
216
|
+
resources: list[Resource],
|
|
217
|
+
world: WorldState,
|
|
218
|
+
) -> TaskGraph:
|
|
219
|
+
goal_action = goal.action.strip().lower()
|
|
220
|
+
if goal_action not in SUPPORTED_GOAL_ACTIONS:
|
|
221
|
+
raise PlanningError(f"unsupported_goal_action:{goal_action}")
|
|
222
|
+
|
|
223
|
+
proposal = parse_agent_plan(payload, expected_goal_id=goal.goal_id)
|
|
224
|
+
counts = Counter(task.action for task in proposal.tasks)
|
|
225
|
+
for action in (Capability.PICK, Capability.MOVE, Capability.PLACE):
|
|
226
|
+
if counts[action] != 1:
|
|
227
|
+
raise StructuredOutputError(
|
|
228
|
+
f"invalid_action_count:{action.value}:{counts[action]}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
compiled: list[TaskNode] = []
|
|
232
|
+
for proposed in proposal.tasks:
|
|
233
|
+
if proposed.action is Capability.PICK:
|
|
234
|
+
contract = ActionContract(
|
|
235
|
+
action=Capability.PICK,
|
|
236
|
+
resource_id=proposed.resource_id,
|
|
237
|
+
object_id=proposed.object_id,
|
|
238
|
+
source=proposed.source,
|
|
239
|
+
preconditions=("robot_ready", "object_available"),
|
|
240
|
+
expected_effects=("object_gripped",),
|
|
241
|
+
constraints=("collision_free", "joint_limits"),
|
|
242
|
+
)
|
|
243
|
+
elif proposed.action is Capability.MOVE:
|
|
244
|
+
contract = ActionContract(
|
|
245
|
+
action=Capability.MOVE,
|
|
246
|
+
resource_id=proposed.resource_id,
|
|
247
|
+
object_id=proposed.object_id,
|
|
248
|
+
destination=proposed.destination,
|
|
249
|
+
preconditions=("robot_ready", "object_gripped"),
|
|
250
|
+
expected_effects=("at_destination",),
|
|
251
|
+
constraints=("collision_free", "joint_limits"),
|
|
252
|
+
)
|
|
253
|
+
elif proposed.action is Capability.PLACE:
|
|
254
|
+
contract = ActionContract(
|
|
255
|
+
action=Capability.PLACE,
|
|
256
|
+
resource_id=proposed.resource_id,
|
|
257
|
+
object_id=proposed.object_id,
|
|
258
|
+
destination=proposed.destination,
|
|
259
|
+
preconditions=("robot_ready", "object_gripped", "at_destination"),
|
|
260
|
+
expected_effects=("object_placed", "goal_complete"),
|
|
261
|
+
constraints=("collision_free", "joint_limits"),
|
|
262
|
+
)
|
|
263
|
+
else: # pragma: no cover - parser currently prevents this
|
|
264
|
+
raise StructuredOutputError(
|
|
265
|
+
f"unsupported_compiled_action:{proposed.action.value}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
compiled.append(
|
|
269
|
+
TaskNode(
|
|
270
|
+
task_id=proposed.task_id,
|
|
271
|
+
contract=contract,
|
|
272
|
+
depends_on=proposed.depends_on,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
graph = TaskGraph(goal_id=proposal.goal_id, tasks=tuple(compiled))
|
|
277
|
+
graph.topological_order()
|
|
278
|
+
semantic = self.semantic_policy.verify(goal, graph, resources, world)
|
|
279
|
+
if not semantic.passed:
|
|
280
|
+
detail = ";".join(
|
|
281
|
+
f"{issue.code}:{issue.task_id or ''}:{issue.detail}"
|
|
282
|
+
for issue in semantic.issues
|
|
283
|
+
)
|
|
284
|
+
raise StructuredOutputError(f"semantic_verification_failed:{detail}")
|
|
285
|
+
return graph
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentRole(str, Enum):
|
|
9
|
+
INTERPRETER = "interpreter"
|
|
10
|
+
PLANNER = "planner"
|
|
11
|
+
CRITIC = "critic"
|
|
12
|
+
RECOVERY = "recovery"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RoutingStrategy(str, Enum):
|
|
16
|
+
AUTO_BEST = "auto_best"
|
|
17
|
+
FIXED = "fixed"
|
|
18
|
+
LOWEST_COST = "lowest_cost"
|
|
19
|
+
LOWEST_LATENCY = "lowest_latency"
|
|
20
|
+
PRIVATE_ONLY = "private_only"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CriticDecision(str, Enum):
|
|
24
|
+
ACCEPT = "accept"
|
|
25
|
+
REVISE = "revise"
|
|
26
|
+
REJECT = "reject"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RecoveryDecision(str, Enum):
|
|
30
|
+
RETRY = "retry"
|
|
31
|
+
REPLAN = "replan"
|
|
32
|
+
WAIT = "wait"
|
|
33
|
+
OBSERVE = "observe"
|
|
34
|
+
ABORT = "abort"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class RoutingPolicy:
|
|
39
|
+
strategy: RoutingStrategy = RoutingStrategy.AUTO_BEST
|
|
40
|
+
fixed_provider: str | None = None
|
|
41
|
+
fixed_model: str | None = None
|
|
42
|
+
max_attempts: int = 3
|
|
43
|
+
timeout_s: float = 30.0
|
|
44
|
+
min_quality: float = 0.0
|
|
45
|
+
require_qualified: bool = True
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if self.fixed_provider is not None:
|
|
49
|
+
object.__setattr__(
|
|
50
|
+
self, "fixed_provider", self.fixed_provider.strip().lower()
|
|
51
|
+
)
|
|
52
|
+
if self.fixed_model is not None:
|
|
53
|
+
object.__setattr__(self, "fixed_model", self.fixed_model.strip())
|
|
54
|
+
|
|
55
|
+
if not 1 <= self.max_attempts <= 8:
|
|
56
|
+
raise ValueError("max_attempts_out_of_range")
|
|
57
|
+
if not 0.1 <= self.timeout_s <= 120.0:
|
|
58
|
+
raise ValueError("timeout_s_out_of_range")
|
|
59
|
+
if not 0.0 <= self.min_quality <= 1.0:
|
|
60
|
+
raise ValueError("min_quality_out_of_range")
|
|
61
|
+
if self.strategy is RoutingStrategy.FIXED and (
|
|
62
|
+
not self.fixed_provider or not self.fixed_model
|
|
63
|
+
):
|
|
64
|
+
raise ValueError("fixed_route_requires_provider_and_model")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class ModelRequest:
|
|
69
|
+
role: AgentRole
|
|
70
|
+
system_instruction: str
|
|
71
|
+
input_payload: Mapping[str, Any]
|
|
72
|
+
output_schema: Mapping[str, Any]
|
|
73
|
+
timeout_s: float
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
if not self.system_instruction.strip():
|
|
77
|
+
raise ValueError("system_instruction_required")
|
|
78
|
+
if not 0.1 <= self.timeout_s <= 120.0:
|
|
79
|
+
raise ValueError("request_timeout_out_of_range")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class ModelResponse:
|
|
84
|
+
provider: str
|
|
85
|
+
model: str
|
|
86
|
+
payload: Mapping[str, Any]
|
|
87
|
+
request_id: str = ""
|
|
88
|
+
latency_ms: float | None = None
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
if not self.provider.strip() or not self.model.strip():
|
|
92
|
+
raise ValueError("response_identity_required")
|
|
93
|
+
if self.latency_ms is not None and self.latency_ms < 0:
|
|
94
|
+
raise ValueError("response_latency_invalid")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class ModelAttempt:
|
|
99
|
+
provider: str
|
|
100
|
+
model: str
|
|
101
|
+
outcome: str
|
|
102
|
+
error_code: str = ""
|
|
103
|
+
latency_ms: float | None = None
|
|
104
|
+
request_id: str = ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class AgentEvidence:
|
|
109
|
+
trace_id: str
|
|
110
|
+
role: AgentRole
|
|
111
|
+
input_hash: str
|
|
112
|
+
output_hash: str | None
|
|
113
|
+
attempts: tuple[ModelAttempt, ...]
|
|
114
|
+
selected_provider: str | None = None
|
|
115
|
+
selected_model: str | None = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class Critique:
|
|
120
|
+
decision: CriticDecision
|
|
121
|
+
score: float
|
|
122
|
+
issues: tuple[str, ...] = ()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True, slots=True)
|
|
126
|
+
class RecoveryAdvice:
|
|
127
|
+
decision: RecoveryDecision
|
|
128
|
+
reason: str
|
|
129
|
+
retry_after_ms: int | None = None
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from .contracts import AgentEvidence, CriticDecision, RoutingPolicy
|
|
7
|
+
from .critic import CriticAgent
|
|
8
|
+
from .evidence import task_graph_hash
|
|
9
|
+
from .planner import PlannerAgent
|
|
10
|
+
from ..models import Goal, Resource, TaskGraph, WorldState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PlanningCoordinationError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class VerifiedPlanArtifact:
|
|
19
|
+
graph: TaskGraph
|
|
20
|
+
graph_hash: str
|
|
21
|
+
planner_evidence: tuple[AgentEvidence, ...]
|
|
22
|
+
critic_evidence: tuple[AgentEvidence, ...]
|
|
23
|
+
revision_rounds: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PlanningCoordinator:
|
|
27
|
+
"""Planner/critic loop with bounded revisions and deterministic plan proof."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
planner: PlannerAgent,
|
|
32
|
+
critic: CriticAgent,
|
|
33
|
+
*,
|
|
34
|
+
max_revision_rounds: int = 2,
|
|
35
|
+
min_accept_score: float = 0.70,
|
|
36
|
+
) -> None:
|
|
37
|
+
if not 0 <= max_revision_rounds <= 4:
|
|
38
|
+
raise ValueError("max_revision_rounds_out_of_range")
|
|
39
|
+
if not 0.0 <= min_accept_score <= 1.0:
|
|
40
|
+
raise ValueError("min_accept_score_out_of_range")
|
|
41
|
+
self.planner = planner
|
|
42
|
+
self.critic = critic
|
|
43
|
+
self.max_revision_rounds = max_revision_rounds
|
|
44
|
+
self.min_accept_score = min_accept_score
|
|
45
|
+
|
|
46
|
+
def create_verified_plan(
|
|
47
|
+
self,
|
|
48
|
+
goal: Goal,
|
|
49
|
+
resources: list[Resource],
|
|
50
|
+
world: WorldState,
|
|
51
|
+
planner_policy: RoutingPolicy,
|
|
52
|
+
critic_policy: RoutingPolicy,
|
|
53
|
+
*,
|
|
54
|
+
engineering_context: Mapping[str, Any] | None = None,
|
|
55
|
+
) -> VerifiedPlanArtifact:
|
|
56
|
+
feedback: tuple[str, ...] = ()
|
|
57
|
+
planner_evidence: list[AgentEvidence] = []
|
|
58
|
+
critic_evidence: list[AgentEvidence] = []
|
|
59
|
+
|
|
60
|
+
for revision in range(self.max_revision_rounds + 1):
|
|
61
|
+
planned = self.planner.plan(
|
|
62
|
+
goal,
|
|
63
|
+
resources,
|
|
64
|
+
world,
|
|
65
|
+
planner_policy,
|
|
66
|
+
feedback=feedback,
|
|
67
|
+
engineering_context=engineering_context,
|
|
68
|
+
)
|
|
69
|
+
planner_evidence.append(planned.evidence)
|
|
70
|
+
|
|
71
|
+
reviewed = self.critic.review(
|
|
72
|
+
goal,
|
|
73
|
+
planned.graph,
|
|
74
|
+
resources,
|
|
75
|
+
world,
|
|
76
|
+
critic_policy,
|
|
77
|
+
engineering_context=engineering_context,
|
|
78
|
+
)
|
|
79
|
+
critic_evidence.append(reviewed.evidence)
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
reviewed.critique.decision is CriticDecision.ACCEPT
|
|
83
|
+
and reviewed.critique.score >= self.min_accept_score
|
|
84
|
+
):
|
|
85
|
+
return VerifiedPlanArtifact(
|
|
86
|
+
graph=planned.graph,
|
|
87
|
+
graph_hash=task_graph_hash(planned.graph),
|
|
88
|
+
planner_evidence=tuple(planner_evidence),
|
|
89
|
+
critic_evidence=tuple(critic_evidence),
|
|
90
|
+
revision_rounds=revision,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if reviewed.critique.decision is CriticDecision.REJECT:
|
|
94
|
+
raise PlanningCoordinationError(
|
|
95
|
+
"critic_rejected_plan:"
|
|
96
|
+
+ ";".join(reviewed.critique.issues)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if reviewed.critique.decision is CriticDecision.ACCEPT:
|
|
100
|
+
feedback = reviewed.critique.issues or (
|
|
101
|
+
f"critic_score_below_threshold:{reviewed.critique.score:.3f}",
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
feedback = reviewed.critique.issues or (
|
|
105
|
+
"critic_requested_revision",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
raise PlanningCoordinationError("revision_budget_exhausted")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from .contracts import AgentEvidence, AgentRole, Critique, RoutingPolicy
|
|
7
|
+
from .evidence import task_graph_to_dict
|
|
8
|
+
from .runtime import AgentRuntime, InvocationResult
|
|
9
|
+
from .structured import CRITIQUE_SCHEMA, json_safe, parse_critique
|
|
10
|
+
from ..models import Goal, Resource, TaskGraph, WorldState
|
|
11
|
+
from ..verification import DeterministicVerifier
|
|
12
|
+
|
|
13
|
+
CRITIC_SYSTEM = """You are the DevAgent planning critic. Review a deterministically valid task plan for efficiency, unnecessary steps, ambiguity, recoverability, robustness, and fit to the validated engineering objective. You are advisory only and cannot mark an unsafe plan safe, weaken hard constraints, invent missing engineering facts, or authorize execution."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class CriticResult:
|
|
18
|
+
critique: Critique
|
|
19
|
+
evidence: AgentEvidence
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CriticAgent:
|
|
23
|
+
"""Advisory critic; deterministic verification remains authoritative."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
runtime: AgentRuntime,
|
|
28
|
+
verifier: DeterministicVerifier | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.runtime = runtime
|
|
31
|
+
self.verifier = verifier or DeterministicVerifier()
|
|
32
|
+
|
|
33
|
+
def review(
|
|
34
|
+
self,
|
|
35
|
+
goal: Goal,
|
|
36
|
+
graph: TaskGraph,
|
|
37
|
+
resources: list[Resource],
|
|
38
|
+
world: WorldState,
|
|
39
|
+
policy: RoutingPolicy,
|
|
40
|
+
*,
|
|
41
|
+
engineering_context: Mapping[str, Any] | None = None,
|
|
42
|
+
) -> CriticResult:
|
|
43
|
+
verified = self.verifier.verify_graph(graph, resources)
|
|
44
|
+
if not verified.passed:
|
|
45
|
+
raise ValueError("critic_requires_deterministically_valid_plan")
|
|
46
|
+
|
|
47
|
+
input_payload = {
|
|
48
|
+
"goal": {
|
|
49
|
+
"goal_id": goal.goal_id,
|
|
50
|
+
"action": goal.action,
|
|
51
|
+
"object_id": goal.object_id,
|
|
52
|
+
"source": goal.source,
|
|
53
|
+
"destination": goal.destination,
|
|
54
|
+
},
|
|
55
|
+
"plan": task_graph_to_dict(graph),
|
|
56
|
+
"world": {
|
|
57
|
+
"facts": sorted(world.facts),
|
|
58
|
+
"values": dict(world.values),
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
if engineering_context is not None:
|
|
62
|
+
input_payload["validated_engineering_context"] = json_safe(engineering_context)
|
|
63
|
+
|
|
64
|
+
invocation: InvocationResult = self.runtime.invoke(
|
|
65
|
+
role=AgentRole.CRITIC,
|
|
66
|
+
system_instruction=CRITIC_SYSTEM,
|
|
67
|
+
input_payload=input_payload,
|
|
68
|
+
output_schema=CRITIQUE_SCHEMA,
|
|
69
|
+
policy=policy,
|
|
70
|
+
validator=parse_critique,
|
|
71
|
+
)
|
|
72
|
+
return CriticResult(invocation.value, invocation.evidence)
|