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.
Files changed (92) hide show
  1. pdl_taskmaster/__init__.py +6 -0
  2. pdl_taskmaster/contracts/AUTHORITY_MAP.json +19 -0
  3. pdl_taskmaster/contracts/CALIBRATION.json +7 -0
  4. pdl_taskmaster/contracts/CONTRACT_MANIFEST.json +231 -0
  5. pdl_taskmaster/contracts/EXECUTION_CONTRACT.json +615 -0
  6. pdl_taskmaster/contracts/IMPLEMENTATION_BOUNDARY.json +32 -0
  7. pdl_taskmaster/contracts/VERIFICATION_CONTRACT.json +379 -0
  8. pdl_taskmaster/contracts/standards/ARTIFACT_STANDARD.md +7 -0
  9. pdl_taskmaster/contracts/standards/AUTHORITY_STANDARD.md +17 -0
  10. pdl_taskmaster/contracts/standards/CONFORMANCE_STANDARD.md +15 -0
  11. pdl_taskmaster/contracts/standards/CONTEXT_STANDARD.md +15 -0
  12. pdl_taskmaster/contracts/standards/EXECUTION_STANDARD.md +14 -0
  13. pdl_taskmaster/contracts/standards/HANDOFF_STANDARD.md +21 -0
  14. pdl_taskmaster/contracts/standards/INSTALLATION_STANDARD.md +17 -0
  15. pdl_taskmaster/contracts/standards/PDL_STANDARD.md +19 -0
  16. pdl_taskmaster/contracts/standards/PROMPT_STANDARD.md +13 -0
  17. pdl_taskmaster/contracts/standards/PROTOCOL_STANDARD.md +17 -0
  18. pdl_taskmaster/contracts/standards/RESPONSE_PLAN_STANDARD.md +24 -0
  19. pdl_taskmaster/contracts/standards/RESULT_STANDARD.md +35 -0
  20. pdl_taskmaster/contracts/standards/REVIEW_STANDARD.md +31 -0
  21. pdl_taskmaster/contracts/standards/SEMANTIC_INPUT_STANDARD.md +15 -0
  22. pdl_taskmaster/contracts/standards/TASK_SEMANTICS_STANDARD.md +9 -0
  23. pdl_taskmaster/controller/__init__.py +0 -0
  24. pdl_taskmaster/controller/mechanical_controller.py +542 -0
  25. pdl_taskmaster/controller/schemas/activation_decision.schema.json +34 -0
  26. pdl_taskmaster/controller/schemas/artifact_body.schema.json +14 -0
  27. pdl_taskmaster/controller/schemas/artifact_review_facts.schema.json +91 -0
  28. pdl_taskmaster/controller/schemas/bootstrap_analysis.schema.json +60 -0
  29. pdl_taskmaster/controller/schemas/execution_draft.schema.json +47 -0
  30. pdl_taskmaster/controller/schemas/execution_input_decision.schema.json +43 -0
  31. pdl_taskmaster/controller/schemas/execution_outcome.schema.json +201 -0
  32. pdl_taskmaster/controller/schemas/plan_artifact.schema.json +14 -0
  33. pdl_taskmaster/controller/schemas/prompt_artifact.schema.json +63 -0
  34. pdl_taskmaster/controller/schemas/protocol_discussion.schema.json +8 -0
  35. pdl_taskmaster/controller/schemas/result_ir_repair.schema.json +140 -0
  36. pdl_taskmaster/eval/__init__.py +1 -0
  37. pdl_taskmaster/eval/build_adversarial_battery.py +351 -0
  38. pdl_taskmaster/eval/compare_eval_runs.py +251 -0
  39. pdl_taskmaster/eval/control_api_call.py +428 -0
  40. pdl_taskmaster/eval/export_rlcd_dataset.py +799 -0
  41. pdl_taskmaster/eval/fidelity_scan.py +268 -0
  42. pdl_taskmaster/eval/gen_fidelity_battery.py +457 -0
  43. pdl_taskmaster/eval/leak_scan.py +260 -0
  44. pdl_taskmaster/eval/rerecord_fixtures.py +139 -0
  45. pdl_taskmaster/eval/run_canary_shape_probe.py +229 -0
  46. pdl_taskmaster/eval/run_dual_gate_probe.py +404 -0
  47. pdl_taskmaster/eval/run_qualified_batch.py +841 -0
  48. pdl_taskmaster/eval/summarize_run.py +194 -0
  49. pdl_taskmaster/host/__init__.py +1 -0
  50. pdl_taskmaster/host/app.py +164 -0
  51. pdl_taskmaster/host/cli.py +100 -0
  52. pdl_taskmaster/host/repl.py +866 -0
  53. pdl_taskmaster/observation/__init__.py +1 -0
  54. pdl_taskmaster/observation/cli.py +47 -0
  55. pdl_taskmaster/observation/observed_session.py +219 -0
  56. pdl_taskmaster/observation/parsers.py +66 -0
  57. pdl_taskmaster/observation/records.py +58 -0
  58. pdl_taskmaster/observation/session.py +3 -0
  59. pdl_taskmaster/observation/sink.py +3 -0
  60. pdl_taskmaster/observation/sinks.py +20 -0
  61. pdl_taskmaster/providers/README.md +66 -0
  62. pdl_taskmaster/providers/__init__.py +1 -0
  63. pdl_taskmaster/providers/api_worker.py +411 -0
  64. pdl_taskmaster/providers/base.py +26 -0
  65. pdl_taskmaster/providers/codex_worker.py +328 -0
  66. pdl_taskmaster/providers/fixtures.py +157 -0
  67. pdl_taskmaster/providers/live_stub.py +66 -0
  68. pdl_taskmaster/providers/recorded.py +120 -0
  69. pdl_taskmaster/py.typed +1 -0
  70. pdl_taskmaster/runtime/__init__.py +0 -0
  71. pdl_taskmaster/runtime/context_compiler.py +151 -0
  72. pdl_taskmaster/runtime/model_classification.py +290 -0
  73. pdl_taskmaster/runtime/normative_store.py +157 -0
  74. pdl_taskmaster/runtime/operation_bridge.py +336 -0
  75. pdl_taskmaster/runtime/presentation.py +18 -0
  76. pdl_taskmaster/runtime/quarantine.py +91 -0
  77. pdl_taskmaster/runtime/result_ir.py +401 -0
  78. pdl_taskmaster/runtime/semantic_observer.py +40 -0
  79. pdl_taskmaster/runtime/session_engine.py +1168 -0
  80. pdl_taskmaster/runtime/standard_registry.py +55 -0
  81. pdl_taskmaster/runtime/wire_payloads.py +462 -0
  82. pdl_taskmaster/runtime/worker-bootstrap.txt +4 -0
  83. pdl_taskmaster/runtime/workspace.py +602 -0
  84. pdl_taskmaster/tracking/__init__.py +1 -0
  85. pdl_taskmaster/tracking/log_live_session.py +98 -0
  86. pdl_taskmaster/tracking/mlflow_sink.py +58 -0
  87. pdl_taskmaster/verify/verify_repl_baseline.py +299 -0
  88. pdl_taskmaster-2.4.0.dist-info/METADATA +155 -0
  89. pdl_taskmaster-2.4.0.dist-info/RECORD +92 -0
  90. pdl_taskmaster-2.4.0.dist-info/WHEEL +4 -0
  91. pdl_taskmaster-2.4.0.dist-info/entry_points.txt +2 -0
  92. 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
+
@@ -0,0 +1,4 @@
1
+ You are a stateless operation worker.
2
+ The supplied projection is the complete invocation for this call.
3
+ Return exactly one JSON object matching the declared output_schema.
4
+ Do not add fields that the selected schema variant does not define.