pdl-taskmaster 2.4.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.
- pdl_taskmaster/__init__.py +6 -0
- pdl_taskmaster/contracts/AUTHORITY_MAP.json +19 -0
- pdl_taskmaster/contracts/CALIBRATION.json +7 -0
- pdl_taskmaster/contracts/CONTRACT_MANIFEST.json +231 -0
- pdl_taskmaster/contracts/EXECUTION_CONTRACT.json +615 -0
- pdl_taskmaster/contracts/IMPLEMENTATION_BOUNDARY.json +32 -0
- pdl_taskmaster/contracts/VERIFICATION_CONTRACT.json +379 -0
- pdl_taskmaster/contracts/standards/ARTIFACT_STANDARD.md +7 -0
- pdl_taskmaster/contracts/standards/AUTHORITY_STANDARD.md +17 -0
- pdl_taskmaster/contracts/standards/CONFORMANCE_STANDARD.md +15 -0
- pdl_taskmaster/contracts/standards/CONTEXT_STANDARD.md +15 -0
- pdl_taskmaster/contracts/standards/EXECUTION_STANDARD.md +14 -0
- pdl_taskmaster/contracts/standards/HANDOFF_STANDARD.md +21 -0
- pdl_taskmaster/contracts/standards/INSTALLATION_STANDARD.md +17 -0
- pdl_taskmaster/contracts/standards/PDL_STANDARD.md +19 -0
- pdl_taskmaster/contracts/standards/PROMPT_STANDARD.md +13 -0
- pdl_taskmaster/contracts/standards/PROTOCOL_STANDARD.md +17 -0
- pdl_taskmaster/contracts/standards/RESPONSE_PLAN_STANDARD.md +24 -0
- pdl_taskmaster/contracts/standards/RESULT_STANDARD.md +35 -0
- pdl_taskmaster/contracts/standards/REVIEW_STANDARD.md +31 -0
- pdl_taskmaster/contracts/standards/SEMANTIC_INPUT_STANDARD.md +15 -0
- pdl_taskmaster/contracts/standards/TASK_SEMANTICS_STANDARD.md +9 -0
- pdl_taskmaster/controller/__init__.py +0 -0
- pdl_taskmaster/controller/mechanical_controller.py +542 -0
- pdl_taskmaster/controller/schemas/activation_decision.schema.json +34 -0
- pdl_taskmaster/controller/schemas/artifact_body.schema.json +14 -0
- pdl_taskmaster/controller/schemas/artifact_review_facts.schema.json +91 -0
- pdl_taskmaster/controller/schemas/bootstrap_analysis.schema.json +60 -0
- pdl_taskmaster/controller/schemas/execution_draft.schema.json +47 -0
- pdl_taskmaster/controller/schemas/execution_input_decision.schema.json +43 -0
- pdl_taskmaster/controller/schemas/execution_outcome.schema.json +201 -0
- pdl_taskmaster/controller/schemas/plan_artifact.schema.json +14 -0
- pdl_taskmaster/controller/schemas/prompt_artifact.schema.json +63 -0
- pdl_taskmaster/controller/schemas/protocol_discussion.schema.json +8 -0
- pdl_taskmaster/controller/schemas/result_ir_repair.schema.json +140 -0
- pdl_taskmaster/eval/__init__.py +1 -0
- pdl_taskmaster/eval/build_adversarial_battery.py +351 -0
- pdl_taskmaster/eval/compare_eval_runs.py +251 -0
- pdl_taskmaster/eval/control_api_call.py +428 -0
- pdl_taskmaster/eval/export_rlcd_dataset.py +799 -0
- pdl_taskmaster/eval/fidelity_scan.py +268 -0
- pdl_taskmaster/eval/gen_fidelity_battery.py +457 -0
- pdl_taskmaster/eval/leak_scan.py +260 -0
- pdl_taskmaster/eval/rerecord_fixtures.py +139 -0
- pdl_taskmaster/eval/run_canary_shape_probe.py +229 -0
- pdl_taskmaster/eval/run_dual_gate_probe.py +404 -0
- pdl_taskmaster/eval/run_qualified_batch.py +841 -0
- pdl_taskmaster/eval/summarize_run.py +194 -0
- pdl_taskmaster/host/__init__.py +1 -0
- pdl_taskmaster/host/app.py +164 -0
- pdl_taskmaster/host/cli.py +100 -0
- pdl_taskmaster/host/repl.py +866 -0
- pdl_taskmaster/observation/__init__.py +1 -0
- pdl_taskmaster/observation/cli.py +47 -0
- pdl_taskmaster/observation/observed_session.py +219 -0
- pdl_taskmaster/observation/parsers.py +66 -0
- pdl_taskmaster/observation/records.py +58 -0
- pdl_taskmaster/observation/session.py +3 -0
- pdl_taskmaster/observation/sink.py +3 -0
- pdl_taskmaster/observation/sinks.py +20 -0
- pdl_taskmaster/providers/README.md +66 -0
- pdl_taskmaster/providers/__init__.py +1 -0
- pdl_taskmaster/providers/api_worker.py +411 -0
- pdl_taskmaster/providers/base.py +26 -0
- pdl_taskmaster/providers/codex_worker.py +328 -0
- pdl_taskmaster/providers/fixtures.py +157 -0
- pdl_taskmaster/providers/live_stub.py +66 -0
- pdl_taskmaster/providers/recorded.py +120 -0
- pdl_taskmaster/py.typed +1 -0
- pdl_taskmaster/runtime/__init__.py +0 -0
- pdl_taskmaster/runtime/context_compiler.py +151 -0
- pdl_taskmaster/runtime/model_classification.py +290 -0
- pdl_taskmaster/runtime/normative_store.py +157 -0
- pdl_taskmaster/runtime/operation_bridge.py +336 -0
- pdl_taskmaster/runtime/presentation.py +18 -0
- pdl_taskmaster/runtime/quarantine.py +91 -0
- pdl_taskmaster/runtime/result_ir.py +401 -0
- pdl_taskmaster/runtime/semantic_observer.py +40 -0
- pdl_taskmaster/runtime/session_engine.py +1168 -0
- pdl_taskmaster/runtime/standard_registry.py +55 -0
- pdl_taskmaster/runtime/wire_payloads.py +462 -0
- pdl_taskmaster/runtime/worker-bootstrap.txt +4 -0
- pdl_taskmaster/runtime/workspace.py +602 -0
- pdl_taskmaster/tracking/__init__.py +1 -0
- pdl_taskmaster/tracking/log_live_session.py +98 -0
- pdl_taskmaster/tracking/mlflow_sink.py +58 -0
- pdl_taskmaster/verify/verify_repl_baseline.py +299 -0
- pdl_taskmaster-2.4.0.dist-info/METADATA +155 -0
- pdl_taskmaster-2.4.0.dist-info/RECORD +92 -0
- pdl_taskmaster-2.4.0.dist-info/WHEEL +4 -0
- pdl_taskmaster-2.4.0.dist-info/entry_points.txt +2 -0
- pdl_taskmaster-2.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_DEFINITION = re.compile(r"^\*\*([A-Z]+-[0-9]{2})\s+—.*$", re.MULTILINE)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class StandardClause:
|
|
14
|
+
requirement_id: str
|
|
15
|
+
text: str
|
|
16
|
+
source: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StandardRegistry:
|
|
20
|
+
def __init__(self, repo_root: str | Path, standards_root: str | Path | None = None):
|
|
21
|
+
self.repo_root = Path(repo_root)
|
|
22
|
+
from pdl_taskmaster.runtime.normative_store import NormativeStore
|
|
23
|
+
self.standards_root = Path(standards_root) if standards_root else NormativeStore.resolve_standards_root(self.repo_root)
|
|
24
|
+
manifest_path = self.standards_root / "CONTRACT_MANIFEST.json"
|
|
25
|
+
if not manifest_path.is_file():
|
|
26
|
+
manifest_path = self.repo_root / "contracts" / "CONTRACT_MANIFEST.json"
|
|
27
|
+
self.manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
28
|
+
self.requirement_index: dict[str, str] = dict(self.manifest["requirement_index"])
|
|
29
|
+
self._clauses = self._load()
|
|
30
|
+
|
|
31
|
+
def _load(self) -> dict[str, StandardClause]:
|
|
32
|
+
result: dict[str, StandardClause] = {}
|
|
33
|
+
for requirement_id, relative in self.requirement_index.items():
|
|
34
|
+
path = self.standards_root.parent / relative
|
|
35
|
+
if not path.is_file():
|
|
36
|
+
rel_clean = relative.removeprefix("contracts/").removeprefix("contracts\\")
|
|
37
|
+
path = self.standards_root / rel_clean
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
path = self.repo_root / relative
|
|
40
|
+
text = path.read_text(encoding="utf-8")
|
|
41
|
+
matches = [line for line in text.splitlines() if line.startswith(f"**{requirement_id} ")]
|
|
42
|
+
if len(matches) != 1:
|
|
43
|
+
raise ValueError(f"requirement_definition:{requirement_id}")
|
|
44
|
+
if not _DEFINITION.match(matches[0]):
|
|
45
|
+
raise ValueError(f"requirement_format:{requirement_id}")
|
|
46
|
+
result[requirement_id] = StandardClause(requirement_id, matches[0], relative)
|
|
47
|
+
if set(result) != set(self.requirement_index):
|
|
48
|
+
raise ValueError("requirement_index")
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
def get(self, requirement_id: str) -> StandardClause:
|
|
52
|
+
return self._clauses[requirement_id]
|
|
53
|
+
|
|
54
|
+
def select(self, requirement_ids: list[str] | tuple[str, ...]) -> tuple[StandardClause, ...]:
|
|
55
|
+
return tuple(self.get(requirement_id) for requirement_id in requirement_ids)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Annotated, Any, Literal, Optional, Union
|
|
5
|
+
from pydantic import (
|
|
6
|
+
BaseModel,
|
|
7
|
+
ConfigDict,
|
|
8
|
+
Field,
|
|
9
|
+
TypeAdapter,
|
|
10
|
+
ValidationError,
|
|
11
|
+
model_validator,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WireError(RuntimeError):
|
|
16
|
+
"""Raised when a model response violates wire syntax or schema contract.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
reason: Legacy or semantic error token (e.g. 'artifact_review_kind', 'task_entities').
|
|
20
|
+
operator_feedback: Formatted, field-localized description for retry correction.
|
|
21
|
+
validation_error: Underlying Pydantic ValidationError if applicable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
reason: str,
|
|
27
|
+
*,
|
|
28
|
+
operator_feedback: str | None = None,
|
|
29
|
+
validation_error: ValidationError | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(reason)
|
|
32
|
+
self.reason = reason
|
|
33
|
+
self.operator_feedback = operator_feedback
|
|
34
|
+
self.validation_error = validation_error
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ActivationRoute(str, Enum):
|
|
38
|
+
APPLY_PROTOCOL = "APPLY_PROTOCOL"
|
|
39
|
+
PROTOCOL_DISCUSSION = "PROTOCOL_DISCUSSION"
|
|
40
|
+
BYPASS = "BYPASS"
|
|
41
|
+
BLOCKED_BY_HIGHER_PRIORITY = "BLOCKED_BY_HIGHER_PRIORITY"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ActivationDecisionPayload(BaseModel):
|
|
45
|
+
model_config = ConfigDict(extra="forbid")
|
|
46
|
+
route: ActivationRoute
|
|
47
|
+
response: Optional[str] = None
|
|
48
|
+
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
49
|
+
|
|
50
|
+
@model_validator(mode="after")
|
|
51
|
+
def validate_route_constraints(self) -> ActivationDecisionPayload:
|
|
52
|
+
if self.route == ActivationRoute.BLOCKED_BY_HIGHER_PRIORITY:
|
|
53
|
+
if not self.response or not self.response.strip():
|
|
54
|
+
raise ValueError("blocked_response: response must be non-empty when route is BLOCKED_BY_HIGHER_PRIORITY")
|
|
55
|
+
else:
|
|
56
|
+
if self.response is not None:
|
|
57
|
+
raise ValueError("extra_fields: response is not permitted unless route is BLOCKED_BY_HIGHER_PRIORITY")
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BootstrapAnalysisData(BaseModel):
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
kind: Literal["ANALYSIS"] = "ANALYSIS"
|
|
64
|
+
task_summary: str
|
|
65
|
+
approach_notes: str
|
|
66
|
+
risk_notes: str
|
|
67
|
+
task_entities: list[str]
|
|
68
|
+
|
|
69
|
+
@model_validator(mode="after")
|
|
70
|
+
def validate_non_empty(self) -> BootstrapAnalysisData:
|
|
71
|
+
if not self.task_summary.strip():
|
|
72
|
+
raise ValueError("bootstrap_task_summary: task_summary must not be empty")
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BootstrapBlockedData(BaseModel):
|
|
77
|
+
model_config = ConfigDict(extra="forbid")
|
|
78
|
+
kind: Literal["BLOCKED_BY_HIGHER_PRIORITY"] = "BLOCKED_BY_HIGHER_PRIORITY"
|
|
79
|
+
response: str
|
|
80
|
+
|
|
81
|
+
@model_validator(mode="after")
|
|
82
|
+
def validate_non_empty(self) -> BootstrapBlockedData:
|
|
83
|
+
if not self.response.strip():
|
|
84
|
+
raise ValueError("blocked_response: response must not be empty")
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
BootstrapAnalysisPayload = Annotated[
|
|
89
|
+
Union[BootstrapAnalysisData, BootstrapBlockedData],
|
|
90
|
+
Field(discriminator="kind"),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PromptDraftData(BaseModel):
|
|
95
|
+
model_config = ConfigDict(extra="forbid")
|
|
96
|
+
kind: Literal["PROMPT"] = "PROMPT"
|
|
97
|
+
prompt_body: str
|
|
98
|
+
approach_handoff: Literal["NONE", "CARRY_SOURCE_TO_PLAN"] = "NONE"
|
|
99
|
+
task_entities: list[str] = Field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
@model_validator(mode="after")
|
|
102
|
+
def validate_body(self) -> PromptDraftData:
|
|
103
|
+
if not self.prompt_body.strip():
|
|
104
|
+
raise ValueError("prompt_body: prompt_body must not be empty")
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PromptDraftBlockedData(BaseModel):
|
|
109
|
+
model_config = ConfigDict(extra="forbid")
|
|
110
|
+
kind: Literal["TASK_BLOCKED_BY_HIGHER_PRIORITY"] = "TASK_BLOCKED_BY_HIGHER_PRIORITY"
|
|
111
|
+
blocking_basis: Literal["PROVIDER_PLATFORM_SAFETY_PRIVACY_PERMISSION_OR_TOOL"]
|
|
112
|
+
response: str
|
|
113
|
+
|
|
114
|
+
@model_validator(mode="after")
|
|
115
|
+
def validate_response(self) -> PromptDraftBlockedData:
|
|
116
|
+
if not self.response.strip():
|
|
117
|
+
raise ValueError("blocked_response: response must not be empty")
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
PromptDraftPayload = Annotated[
|
|
122
|
+
Union[PromptDraftData, PromptDraftBlockedData],
|
|
123
|
+
Field(discriminator="kind"),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PromptBodyPayload(BaseModel):
|
|
128
|
+
model_config = ConfigDict(extra="forbid")
|
|
129
|
+
prompt_body: str
|
|
130
|
+
|
|
131
|
+
@model_validator(mode="after")
|
|
132
|
+
def validate_body(self) -> PromptBodyPayload:
|
|
133
|
+
if not self.prompt_body.strip():
|
|
134
|
+
raise ValueError("prompt_body: prompt_body must not be empty")
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class NeutralPlanBodyPayload(BaseModel):
|
|
139
|
+
model_config = ConfigDict(extra="forbid")
|
|
140
|
+
neutral_plan_body: str
|
|
141
|
+
|
|
142
|
+
@model_validator(mode="after")
|
|
143
|
+
def validate_body(self) -> NeutralPlanBodyPayload:
|
|
144
|
+
if not self.neutral_plan_body.strip():
|
|
145
|
+
raise ValueError("neutral_plan_body: neutral_plan_body must not be empty")
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ProtocolDiscussionPayload(BaseModel):
|
|
150
|
+
model_config = ConfigDict(extra="forbid")
|
|
151
|
+
body: str
|
|
152
|
+
|
|
153
|
+
@model_validator(mode="after")
|
|
154
|
+
def validate_body(self) -> ProtocolDiscussionPayload:
|
|
155
|
+
if not self.body.strip():
|
|
156
|
+
raise ValueError("protocol_body: body must not be empty")
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TaskChangeDimension(str, Enum):
|
|
161
|
+
ACTION_SUBJECT_OR_OBJECT = "ACTION_SUBJECT_OR_OBJECT"
|
|
162
|
+
SCOPE_CONSTRAINT_EXCLUSION_OR_PRIORITY = "SCOPE_CONSTRAINT_EXCLUSION_OR_PRIORITY"
|
|
163
|
+
TIME_FRESHNESS_QUANTITY_OR_CONDITION = "TIME_FRESHNESS_QUANTITY_OR_CONDITION"
|
|
164
|
+
COMPARISON_CRITERION_DEFINITION_OR_RELATIONSHIP = "COMPARISON_CRITERION_DEFINITION_OR_RELATIONSHIP"
|
|
165
|
+
AUDIENCE_OR_OUTPUT_CHARACTERISTIC = "AUDIENCE_OR_OUTPUT_CHARACTERISTIC"
|
|
166
|
+
REQUIRED_CONCLUSION = "REQUIRED_CONCLUSION"
|
|
167
|
+
OTHER_TASK_OR_RESULT = "OTHER_TASK_OR_RESULT"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ApproachChangeDimension(str, Enum):
|
|
171
|
+
RESEARCH_OR_EVIDENCE_SELECTION_METHOD = "RESEARCH_OR_EVIDENCE_SELECTION_METHOD"
|
|
172
|
+
COMPARISON_RANKING_OR_SCORING_METHOD = "COMPARISON_RANKING_OR_SCORING_METHOD"
|
|
173
|
+
ANALYSIS_ORDER = "ANALYSIS_ORDER"
|
|
174
|
+
JUSTIFICATION_PROCEDURE = "JUSTIFICATION_PROCEDURE"
|
|
175
|
+
OTHER_RESPONSE_PROCEDURE = "OTHER_RESPONSE_PROCEDURE"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
SYSTEM1_CONFIDENCE_FLOOR: float = 0.85
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ReviewFactsData(BaseModel):
|
|
182
|
+
model_config = ConfigDict(extra="forbid")
|
|
183
|
+
kind: Literal["REVIEW_FACTS"] = "REVIEW_FACTS"
|
|
184
|
+
task_change_dimensions: list[TaskChangeDimension]
|
|
185
|
+
approach_change_dimensions: list[ApproachChangeDimension]
|
|
186
|
+
progression_requested: bool
|
|
187
|
+
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
188
|
+
|
|
189
|
+
@model_validator(mode="after")
|
|
190
|
+
def validate_deduplication(self) -> ReviewFactsData:
|
|
191
|
+
if len(self.task_change_dimensions) != len(set(self.task_change_dimensions)):
|
|
192
|
+
raise ValueError("task_change_dimensions: duplicates not allowed")
|
|
193
|
+
if len(self.approach_change_dimensions) != len(set(self.approach_change_dimensions)):
|
|
194
|
+
raise ValueError("approach_change_dimensions: duplicates not allowed")
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ReviewSpecialData(BaseModel):
|
|
199
|
+
model_config = ConfigDict(extra="forbid")
|
|
200
|
+
kind: Literal[
|
|
201
|
+
"NEW_TASK",
|
|
202
|
+
"CANCEL",
|
|
203
|
+
"PROTOCOL_DISCUSSION",
|
|
204
|
+
"SUBSTANTIVE_DISCUSSION",
|
|
205
|
+
"UNRESOLVED",
|
|
206
|
+
]
|
|
207
|
+
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
ArtifactReviewPayload = Annotated[
|
|
211
|
+
Union[ReviewFactsData, ReviewSpecialData],
|
|
212
|
+
Field(discriminator="kind"),
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ExecutionInputReviseData(BaseModel):
|
|
217
|
+
model_config = ConfigDict(extra="forbid")
|
|
218
|
+
kind: Literal["REVISE_TASK"] = "REVISE_TASK"
|
|
219
|
+
also_changes_approach: bool
|
|
220
|
+
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ExecutionInputSpecialData(BaseModel):
|
|
224
|
+
model_config = ConfigDict(extra="forbid")
|
|
225
|
+
kind: Literal["SUPPLY_EXECUTION_INPUT", "NEW_TASK", "CANCEL", "UNRESOLVED"]
|
|
226
|
+
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
ExecutionInputPayload = Annotated[
|
|
230
|
+
Union[ExecutionInputReviseData, ExecutionInputSpecialData],
|
|
231
|
+
Field(discriminator="kind"),
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ExecutionDraftBlockedData(BaseModel):
|
|
236
|
+
model_config = ConfigDict(extra="forbid")
|
|
237
|
+
kind: Literal["BLOCKED_BY_HIGHER_PRIORITY"] = "BLOCKED_BY_HIGHER_PRIORITY"
|
|
238
|
+
brief_body: str
|
|
239
|
+
|
|
240
|
+
@model_validator(mode="after")
|
|
241
|
+
def validate_body(self) -> ExecutionDraftBlockedData:
|
|
242
|
+
if not self.brief_body.strip():
|
|
243
|
+
raise ValueError("execution_draft_body: brief_body must not be empty")
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ExecutionDraftResultData(BaseModel):
|
|
248
|
+
model_config = ConfigDict(extra="forbid")
|
|
249
|
+
kind: Literal["RESULT"] = "RESULT"
|
|
250
|
+
brief_body: str
|
|
251
|
+
execution_entities: list[Union[dict[str, Any], str]] = Field(default_factory=list)
|
|
252
|
+
|
|
253
|
+
@model_validator(mode="after")
|
|
254
|
+
def validate_body(self) -> ExecutionDraftResultData:
|
|
255
|
+
if not self.brief_body.strip():
|
|
256
|
+
raise ValueError("execution_draft_body: brief_body must not be empty")
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
ExecutionDraftPayload = Annotated[
|
|
261
|
+
Union[ExecutionDraftBlockedData, ExecutionDraftResultData],
|
|
262
|
+
Field(discriminator="kind"),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ResultIRData(BaseModel):
|
|
267
|
+
model_config = ConfigDict(extra="allow")
|
|
268
|
+
files: list[Any]
|
|
269
|
+
reconciliation: list[Any]
|
|
270
|
+
open_defects: list[Any]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ResultIRRepairPayload(BaseModel):
|
|
274
|
+
model_config = ConfigDict(extra="forbid")
|
|
275
|
+
result_ir: ResultIRData
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ExecutionRequestInputData(BaseModel):
|
|
279
|
+
model_config = ConfigDict(extra="forbid")
|
|
280
|
+
kind: Literal["REQUEST_INPUT"] = "REQUEST_INPUT"
|
|
281
|
+
body: str
|
|
282
|
+
expected_type: str
|
|
283
|
+
description: str
|
|
284
|
+
|
|
285
|
+
@model_validator(mode="after")
|
|
286
|
+
def validate_fields(self) -> ExecutionRequestInputData:
|
|
287
|
+
if not self.body.strip():
|
|
288
|
+
raise ValueError("execution_body: body must not be empty")
|
|
289
|
+
if not self.expected_type.strip():
|
|
290
|
+
raise ValueError("execution_expected_type: expected_type must not be empty")
|
|
291
|
+
if not self.description.strip():
|
|
292
|
+
raise ValueError("execution_description: description must not be empty")
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class ExecutionResultData(BaseModel):
|
|
297
|
+
model_config = ConfigDict(extra="forbid")
|
|
298
|
+
kind: Literal["RESULT", "BLOCKED_BY_HIGHER_PRIORITY"]
|
|
299
|
+
body: str
|
|
300
|
+
result_ir: Optional[ResultIRData] = None
|
|
301
|
+
|
|
302
|
+
@model_validator(mode="after")
|
|
303
|
+
def validate_fields(self) -> ExecutionResultData:
|
|
304
|
+
if not self.body.strip():
|
|
305
|
+
raise ValueError("execution_body: body must not be empty")
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
ExecutionOutcomePayload = Annotated[
|
|
310
|
+
Union[ExecutionRequestInputData, ExecutionResultData],
|
|
311
|
+
Field(discriminator="kind"),
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
OPERATION_PAYLOAD_MODELS: dict[str, Any] = {
|
|
316
|
+
"INTERPRET_ACTIVATION": ActivationDecisionPayload,
|
|
317
|
+
"BOOTSTRAP_ANALYSIS": BootstrapAnalysisPayload,
|
|
318
|
+
"DRAFT_PROMPT": PromptDraftPayload,
|
|
319
|
+
"DRAFT_PLAN": NeutralPlanBodyPayload,
|
|
320
|
+
"REVISE_PROMPT": PromptBodyPayload,
|
|
321
|
+
"REVISE_PLAN": NeutralPlanBodyPayload,
|
|
322
|
+
"INTERPRET_PROMPT_REVIEW": ArtifactReviewPayload,
|
|
323
|
+
"INTERPRET_PLAN_REVIEW": ArtifactReviewPayload,
|
|
324
|
+
"INTERPRET_EXECUTION_INPUT": ExecutionInputPayload,
|
|
325
|
+
"ANSWER_PROTOCOL_DISCUSSION": ProtocolDiscussionPayload,
|
|
326
|
+
"DRAFT_EXECUTION": ExecutionDraftPayload,
|
|
327
|
+
"EMIT_RESULT_IR": ResultIRRepairPayload,
|
|
328
|
+
"EXECUTE": ExecutionOutcomePayload,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_operation_pydantic_schema(operation: str) -> dict[str, Any] | None:
|
|
333
|
+
"""Derive strict JSON Schema for an operation from its Pydantic payload model."""
|
|
334
|
+
model_or_adapter = OPERATION_PAYLOAD_MODELS.get(operation)
|
|
335
|
+
if model_or_adapter is None:
|
|
336
|
+
return None
|
|
337
|
+
adapter = TypeAdapter(model_or_adapter)
|
|
338
|
+
return adapter.json_schema()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def format_validation_feedback(val_err: ValidationError) -> str:
|
|
342
|
+
"""Format Pydantic ValidationError into precise operator feedback."""
|
|
343
|
+
parts: list[str] = []
|
|
344
|
+
strip_tags = {
|
|
345
|
+
"ANALYSIS",
|
|
346
|
+
"PROMPT",
|
|
347
|
+
"BLOCKED_BY_HIGHER_PRIORITY",
|
|
348
|
+
"TASK_BLOCKED_BY_HIGHER_PRIORITY",
|
|
349
|
+
"REVIEW_FACTS",
|
|
350
|
+
"REVISE_TASK",
|
|
351
|
+
"RESULT",
|
|
352
|
+
"REQUEST_INPUT",
|
|
353
|
+
}
|
|
354
|
+
for err in val_err.errors():
|
|
355
|
+
clean_loc = [str(p) for p in err.get("loc", ()) if str(p) not in strip_tags]
|
|
356
|
+
loc = ".".join(clean_loc) or "root"
|
|
357
|
+
msg = err.get("msg", "invalid")
|
|
358
|
+
# Strip internal Pydantic prefixes if present
|
|
359
|
+
if msg.startswith("Value error, "):
|
|
360
|
+
msg = msg[len("Value error, "):]
|
|
361
|
+
parts.append(f"field '{loc}': {msg}")
|
|
362
|
+
return "Validation failed on " + "; ".join(parts)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def map_validation_error_to_wire_reason(
|
|
366
|
+
operation: str,
|
|
367
|
+
val_err: ValidationError,
|
|
368
|
+
raw_dict: dict[str, Any] | None = None,
|
|
369
|
+
) -> str:
|
|
370
|
+
"""Map Pydantic ValidationError to a canonical WireError reason token."""
|
|
371
|
+
errors = val_err.errors()
|
|
372
|
+
if not errors:
|
|
373
|
+
return "schema_violation"
|
|
374
|
+
|
|
375
|
+
# Prioritize union tag / discriminator failures
|
|
376
|
+
for e in errors:
|
|
377
|
+
loc = tuple(str(x) for x in e.get("loc", ()))
|
|
378
|
+
err_type = e.get("type", "")
|
|
379
|
+
msg = e.get("msg", "")
|
|
380
|
+
|
|
381
|
+
if "union_tag_invalid" in err_type or (loc and loc[-1] == "kind") or "discriminator" in msg:
|
|
382
|
+
if operation in {"INTERPRET_PROMPT_REVIEW", "INTERPRET_PLAN_REVIEW"}:
|
|
383
|
+
return "artifact_review_kind"
|
|
384
|
+
if operation == "BOOTSTRAP_ANALYSIS":
|
|
385
|
+
return "bootstrap_kind"
|
|
386
|
+
if operation == "DRAFT_PROMPT":
|
|
387
|
+
return "prompt_draft_kind"
|
|
388
|
+
if operation == "INTERPRET_EXECUTION_INPUT":
|
|
389
|
+
return "execution_input_kind"
|
|
390
|
+
if operation == "DRAFT_EXECUTION":
|
|
391
|
+
return "execution_draft_kind"
|
|
392
|
+
if operation == "EXECUTE":
|
|
393
|
+
return "execution_kind"
|
|
394
|
+
|
|
395
|
+
if err_type == "extra_forbidden" or "extra_fields" in msg:
|
|
396
|
+
return "extra_fields"
|
|
397
|
+
|
|
398
|
+
# Next check if any error is missing_fields
|
|
399
|
+
for e in errors:
|
|
400
|
+
if e.get("type") == "missing":
|
|
401
|
+
return "missing_fields"
|
|
402
|
+
|
|
403
|
+
# Inspect first error details
|
|
404
|
+
first = errors[0]
|
|
405
|
+
loc = tuple(str(x) for x in first.get("loc", ()))
|
|
406
|
+
msg = first.get("msg", "")
|
|
407
|
+
|
|
408
|
+
if "task_change_dimensions" in loc or "task_change_dimensions" in msg:
|
|
409
|
+
return "task_change_dimensions"
|
|
410
|
+
if "approach_change_dimensions" in loc or "approach_change_dimensions" in msg:
|
|
411
|
+
return "approach_change_dimensions"
|
|
412
|
+
if "progression_requested" in loc or "progression_requested" in msg:
|
|
413
|
+
return "progression_requested"
|
|
414
|
+
if "task_entities" in loc or "task_entities" in msg:
|
|
415
|
+
return "bootstrap_task_entities" if operation == "BOOTSTRAP_ANALYSIS" else "task_entities"
|
|
416
|
+
if "task_summary" in loc or "task_summary" in msg:
|
|
417
|
+
return "bootstrap_task_summary"
|
|
418
|
+
if "approach_notes" in loc or "approach_notes" in msg:
|
|
419
|
+
return "bootstrap_approach_notes"
|
|
420
|
+
if "risk_notes" in loc or "risk_notes" in msg:
|
|
421
|
+
return "bootstrap_risk_notes"
|
|
422
|
+
if "route" in loc:
|
|
423
|
+
return "activation_route"
|
|
424
|
+
if "response" in loc or "blocked_response" in msg:
|
|
425
|
+
return "blocked_response"
|
|
426
|
+
if "prompt_body" in loc or "prompt_body" in msg:
|
|
427
|
+
return "prompt_body"
|
|
428
|
+
if "neutral_plan_body" in loc or "neutral_plan_body" in msg:
|
|
429
|
+
return "neutral_plan_body"
|
|
430
|
+
if "body" in loc or "body" in msg:
|
|
431
|
+
return "protocol_body" if operation == "ANSWER_PROTOCOL_DISCUSSION" else "execution_body"
|
|
432
|
+
if "approach_handoff" in loc or "approach_handoff" in msg:
|
|
433
|
+
return "approach_handoff"
|
|
434
|
+
if "blocking_basis" in loc or "blocking_basis" in msg:
|
|
435
|
+
return "blocking_basis"
|
|
436
|
+
if "also_changes_approach" in loc or "also_changes_approach" in msg:
|
|
437
|
+
return "execution_input_also_changes_approach"
|
|
438
|
+
if "brief_body" in loc or "brief_body" in msg:
|
|
439
|
+
return "execution_draft_body"
|
|
440
|
+
if "execution_entities" in loc or "execution_entities" in msg:
|
|
441
|
+
return "execution_draft_entities"
|
|
442
|
+
if "expected_type" in loc or "expected_type" in msg:
|
|
443
|
+
return "execution_expected_type"
|
|
444
|
+
if "description" in loc or "description" in msg:
|
|
445
|
+
return "execution_description"
|
|
446
|
+
if "result_ir" in loc or "result_ir" in msg:
|
|
447
|
+
for leaf in ("files", "reconciliation", "open_defects"):
|
|
448
|
+
if leaf in loc or leaf in msg:
|
|
449
|
+
prefix = "result_ir_repair" if operation == "EMIT_RESULT_IR" else "execution_result_ir"
|
|
450
|
+
return f"{prefix}_{leaf}"
|
|
451
|
+
prefix = "result_ir_repair" if operation == "EMIT_RESULT_IR" else "execution_result_ir"
|
|
452
|
+
return f"{prefix}_shape"
|
|
453
|
+
|
|
454
|
+
if "extra_fields" in msg:
|
|
455
|
+
return "extra_fields"
|
|
456
|
+
if "missing_fields" in msg:
|
|
457
|
+
return "missing_fields"
|
|
458
|
+
if "blocked_response" in msg:
|
|
459
|
+
return "blocked_response"
|
|
460
|
+
|
|
461
|
+
return "schema_violation"
|
|
462
|
+
|