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,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)