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,328 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import threading
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Any, Callable
13
+
14
+ from pdl_taskmaster.providers.base import TransportError, WorkerResult
15
+
16
+
17
+ class CodexWorker:
18
+ """LIVE SEMANTIC WORKER backed by the Codex CLI.
19
+
20
+ This adapter is a semantic worker only. It is NOT a coding domain
21
+ execution backend: it does not own PDL wire validity, controller routing,
22
+ or filesystem artifact production on behalf of the candidate.
23
+
24
+ Label: DEVELOPMENT / LIVE DEMONSTRATION WORKER
25
+ NOT A QUALIFIED R2S MEASUREMENT CONDITION.
26
+
27
+ Safety defaults (normal local operation):
28
+ approval mode = never
29
+ sandbox mode = read-only
30
+ dangerous bypass = OFF
31
+
32
+ The bypass condition is a separate, explicitly opt-in execution
33
+ environment intended only for externally hardened/disposable runners. It
34
+ is never used as an automatic fallback.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ model: str | None = None,
41
+ workdir: str | Path | None = None,
42
+ allowed_workdir_root: str | Path | None = None,
43
+ timeout: float = 600.0,
44
+ progress_path: str | Path | None = None,
45
+ on_progress: Callable[[str], None] | None = None,
46
+ capture_tokens: bool = True,
47
+ sandbox_mode: str = "read-only",
48
+ approval_policy: str = "never",
49
+ allow_bypass: bool = False,
50
+ json_mode: bool | None = None,
51
+ config_overrides: list[str] | None = None,
52
+ ):
53
+ if sandbox_mode not in {"read-only", "workspace-write"}:
54
+ raise ValueError(f"unsupported sandbox mode: {sandbox_mode}")
55
+ if approval_policy not in {"never"}:
56
+ raise ValueError(f"unsupported approval policy: {approval_policy}")
57
+ initial_workdir = str(Path(workdir).resolve()) if workdir else str(Path.cwd().resolve())
58
+ self.model = model
59
+ self.timeout = timeout
60
+ self.progress_path = Path(progress_path) if progress_path else None
61
+ self.on_progress = on_progress
62
+ self.capture_tokens = capture_tokens
63
+ self.sandbox_mode = sandbox_mode
64
+ self.approval_policy = approval_policy
65
+ self.allow_bypass = allow_bypass
66
+ self.json_mode = capture_tokens if json_mode is None else json_mode
67
+ self.config_overrides = list(config_overrides or [])
68
+ self.worker_profile = "codex"
69
+ self.effective_model: str | None = self._read_effective_model()
70
+ self.observed_model: str | None = None
71
+ self.allowed_workdir_root = str(
72
+ Path(allowed_workdir_root).resolve() if allowed_workdir_root else Path(initial_workdir).resolve()
73
+ )
74
+ self.workdir = initial_workdir
75
+ self.codex_cli_version = self._detect_version()
76
+
77
+ @staticmethod
78
+ def _detect_version() -> str:
79
+ try:
80
+ proc = subprocess.run(
81
+ ["codex", "--version"],
82
+ capture_output=True,
83
+ text=True,
84
+ encoding="utf-8",
85
+ errors="replace",
86
+ timeout=15,
87
+ )
88
+ return (proc.stdout or proc.stderr or "").strip() or "unknown"
89
+ except Exception:
90
+ return "unknown"
91
+
92
+ @staticmethod
93
+ def _read_effective_model() -> str | None:
94
+ try:
95
+ config_path = Path.home() / ".codex" / "config.toml"
96
+ if not config_path.is_file():
97
+ return None
98
+ for line in config_path.read_text(encoding="utf-8").splitlines():
99
+ stripped = line.strip()
100
+ if not stripped or stripped.startswith("#"):
101
+ continue
102
+ match = re.match(r'^model\s*=\s*"([^"]+)"', stripped)
103
+ if match:
104
+ return match.group(1)
105
+ except Exception:
106
+ return None
107
+ return None
108
+
109
+ @property
110
+ def workdir(self) -> str:
111
+ return self._workdir
112
+
113
+ @workdir.setter
114
+ def workdir(self, value: str | Path) -> None:
115
+ resolved = str(Path(value).resolve())
116
+ allowed = Path(self.allowed_workdir_root).resolve()
117
+ candidate = Path(resolved).resolve()
118
+ if candidate != allowed and not candidate.is_relative_to(allowed):
119
+ raise ValueError(f"workdir must stay inside allowed root {allowed}: {resolved}")
120
+ self._workdir = resolved
121
+
122
+ @staticmethod
123
+ def _format_progress(line: str, json_mode: bool) -> str:
124
+ if not json_mode or not line.startswith("{"):
125
+ return line
126
+ try:
127
+ event = json.loads(line)
128
+ except Exception:
129
+ return line
130
+ event_type = event.get("type") or "event"
131
+ payload = event.get("payload")
132
+ if isinstance(payload, dict):
133
+ message = payload.get("message") or payload.get("text")
134
+ if isinstance(message, str) and message.strip():
135
+ message = message.strip().replace("\n", " ")
136
+ if len(message) > 240:
137
+ message = message[:237] + "..."
138
+ return f"{event_type}: {message}"
139
+ return event_type
140
+
141
+ def call(self, request: Any) -> WorkerResult:
142
+ prompt = (
143
+ request.prompt
144
+ + "\n\nReturn only a JSON object. Do not include markdown fences, commentary, or extra text."
145
+ )
146
+ fd, output_path = tempfile.mkstemp(prefix="codex-worker-", suffix=".txt")
147
+ os.close(fd)
148
+ output_lines: list[str] = []
149
+ started = time.perf_counter()
150
+ try:
151
+ Path(self.workdir).mkdir(parents=True, exist_ok=True)
152
+ cmd = [
153
+ "codex",
154
+ "exec",
155
+ "-o",
156
+ output_path,
157
+ "--ephemeral",
158
+ "--skip-git-repo-check",
159
+ "-C",
160
+ self.workdir,
161
+ ]
162
+ if self.model:
163
+ cmd += ["-m", self.model]
164
+ if self.allow_bypass:
165
+ # Separate hardened/disposable execution condition only.
166
+ cmd.append("--dangerously-bypass-approvals-and-sandbox")
167
+ else:
168
+ cmd += ["--sandbox", self.sandbox_mode, "-c", 'approval_policy="never"']
169
+ for override in self.config_overrides:
170
+ cmd += ["-c", override]
171
+ if self.json_mode:
172
+ cmd.append("--json")
173
+ cmd.append(prompt)
174
+ popen_kwargs: dict[str, Any] = {}
175
+ if sys.platform != "win32":
176
+ popen_kwargs["start_new_session"] = True
177
+ proc = subprocess.Popen(
178
+ cmd,
179
+ stdout=subprocess.PIPE,
180
+ stderr=subprocess.STDOUT,
181
+ text=True,
182
+ encoding="utf-8",
183
+ errors="replace",
184
+ **popen_kwargs,
185
+ )
186
+
187
+ def _stream() -> None:
188
+ assert proc.stdout is not None
189
+ for line in proc.stdout:
190
+ text_line = line.rstrip()
191
+ output_lines.append(text_line)
192
+ if self.progress_path is not None:
193
+ self.progress_path.parent.mkdir(parents=True, exist_ok=True)
194
+ with self.progress_path.open("a", encoding="utf-8", newline="\n") as handle:
195
+ handle.write(text_line + "\n")
196
+ if self.on_progress is not None:
197
+ try:
198
+ display = self._format_progress(text_line, self.json_mode)
199
+ if display.strip():
200
+ self.on_progress(display)
201
+ except Exception:
202
+ pass
203
+
204
+ thread = threading.Thread(target=_stream, daemon=True)
205
+ thread.start()
206
+ try:
207
+ proc.wait(timeout=self.timeout)
208
+ except subprocess.TimeoutExpired as exc:
209
+ if sys.platform == "win32":
210
+ subprocess.run(
211
+ ["taskkill", "/PID", str(proc.pid), "/T", "/F"],
212
+ capture_output=True,
213
+ text=True,
214
+ encoding="utf-8",
215
+ errors="replace",
216
+ )
217
+ else:
218
+ import signal
219
+ try:
220
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
221
+ except OSError:
222
+ proc.kill()
223
+ thread.join(timeout=5)
224
+ raise TransportError(f"Codex worker timed out after {self.timeout}s") from exc
225
+ thread.join(timeout=5)
226
+ if proc.returncode != 0:
227
+ raise TransportError(
228
+ f"Codex exec failed rc={proc.returncode}; see progress log for details"
229
+ )
230
+ text = Path(output_path).read_text(encoding="utf-8", errors="replace").strip()
231
+ if not text:
232
+ raise TransportError("Codex returned an empty worker response")
233
+
234
+ latency_ms = (time.perf_counter() - started) * 1000.0
235
+ usage, usage_source, usage_exact, response_id, observed_model = self._parse_telemetry(output_lines)
236
+ self.observed_model = observed_model
237
+ metadata: dict[str, Any] = {
238
+ "worker": "codex",
239
+ "model": self.model,
240
+ "observed_model": observed_model,
241
+ "mode": "live-demonstration",
242
+ "not_a_qualified_measurement_condition": True,
243
+ "sandbox_mode": "bypass" if self.allow_bypass else self.sandbox_mode,
244
+ "approval_mode": "bypass" if self.allow_bypass else self.approval_policy,
245
+ "codex_cli_version": self.codex_cli_version,
246
+ "workdir": self.workdir,
247
+ "json_mode": self.json_mode,
248
+ "response_id": response_id,
249
+ "latency_ms": round(latency_ms, 3),
250
+ "usage_source": usage_source,
251
+ "usage_exact": usage_exact,
252
+ "token_telemetry": "enabled" if self.capture_tokens else "disabled",
253
+ }
254
+ if usage:
255
+ metadata["usage"] = usage
256
+ metadata["codex_cli_reported_total"] = usage.get("codex_cli_reported_total")
257
+ return WorkerResult(text, metadata)
258
+ finally:
259
+ try:
260
+ os.unlink(output_path)
261
+ except OSError:
262
+ pass
263
+
264
+ def _parse_telemetry(self, output_lines: list[str]) -> tuple[dict[str, Any] | None, str | None, bool, str | None, str | None]:
265
+ usage: dict[str, Any] | None = None
266
+ usage_source: str | None = None
267
+ usage_exact = False
268
+ response_id: str | None = None
269
+ observed_model: str | None = None
270
+
271
+ if self.capture_tokens and self.json_mode:
272
+ for line in output_lines:
273
+ if not line.startswith("{"):
274
+ continue
275
+ try:
276
+ event = json.loads(line)
277
+ except Exception:
278
+ continue
279
+ payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
280
+ candidate: dict[str, Any] | None = None
281
+ if isinstance(event.get("usage"), dict):
282
+ candidate = event["usage"]
283
+ elif isinstance(payload.get("usage"), dict):
284
+ candidate = payload["usage"]
285
+ if candidate is not None:
286
+ structured: dict[str, Any] = {}
287
+ for key, value in candidate.items():
288
+ if isinstance(value, (int, float)) and key in {
289
+ "input_tokens",
290
+ "output_tokens",
291
+ "total_tokens",
292
+ "prompt_tokens",
293
+ "completion_tokens",
294
+ "reasoning_tokens",
295
+ "cached_input_tokens",
296
+ "uncached_input_tokens",
297
+ "tokens",
298
+ "tokens_used",
299
+ }:
300
+ structured[f"codex_cli_{key}"] = value
301
+ if structured:
302
+ usage = structured
303
+ usage_source = "codex_cli_json"
304
+ usage_exact = True
305
+ if response_id is None:
306
+ for key in ("response_id", "id", "session_id"):
307
+ if isinstance(event.get(key), str):
308
+ response_id = event[key]
309
+ break
310
+ if response_id is None:
311
+ for key in ("response_id", "id", "session_id"):
312
+ if isinstance(payload.get(key), str):
313
+ response_id = payload[key]
314
+ break
315
+ if observed_model is None and isinstance(event.get("model"), str):
316
+ observed_model = event["model"]
317
+ elif observed_model is None and isinstance(payload.get("model"), str):
318
+ observed_model = payload["model"]
319
+
320
+ if usage_source is None and self.capture_tokens:
321
+ for output_line in reversed(output_lines):
322
+ match = re.search(r"tokens used\s*([\d,]+)", output_line, re.IGNORECASE)
323
+ if match:
324
+ usage = {"codex_cli_reported_total": int(match.group(1).replace(",", ""))}
325
+ usage_source = "codex_cli_plaintext"
326
+ usage_exact = False
327
+ break
328
+ return usage, usage_source, usage_exact, response_id, observed_model
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+ import hashlib
7
+ import sys
8
+ import tempfile
9
+
10
+ from pdl_taskmaster.providers.recorded import RecordedFixtureBuilder, RecordedWorker
11
+
12
+
13
+ def _load_jsonl(path: Path) -> list[dict[str, Any]]:
14
+ return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
15
+
16
+
17
+ def _load_candidate(candidate_repo: str | Path) -> str:
18
+ repo = str(Path(candidate_repo).resolve())
19
+ if repo not in sys.path:
20
+ sys.path.insert(0, repo)
21
+ return repo
22
+
23
+
24
+ def _ordered_recorded_calls(workspaces: list[Path]) -> list[dict[str, Any]]:
25
+ calls: list[dict[str, Any]] = []
26
+ for workspace in workspaces:
27
+ workspace_calls: list[dict[str, Any]] = []
28
+ response_paths = list(workspace.glob("stages/*/output/*/model-response.txt")) + list(
29
+ workspace.glob("turns/*/stages/*/output/*/model-response.txt")
30
+ )
31
+ for response_path in response_paths:
32
+ projection_path = response_path.parent / "compiled-projection.json"
33
+ if not projection_path.is_file():
34
+ continue
35
+ projection = json.loads(projection_path.read_text(encoding="utf-8"))
36
+ workspace_calls.append(
37
+ {
38
+ "operation": projection.get("operation", "UNKNOWN"),
39
+ "response": response_path.read_text(encoding="utf-8"),
40
+ "invocation_id": response_path.parent.name,
41
+ }
42
+ )
43
+ workspace_calls.sort(key=lambda item: item["invocation_id"])
44
+ calls.extend(workspace_calls)
45
+ return calls
46
+
47
+
48
+ def _resolve_workspaces(eval_root: Path, row: dict[str, Any]) -> list[Path]:
49
+ base = eval_root / "evidence" / "round4" / "workspaces" / row["run_id"] / row["case_id"]
50
+ candidates = sorted(base.glob("W-*")) if base.is_dir() else []
51
+ if not candidates:
52
+ raise SystemExit(f"cannot resolve workspaces for case {row.get('case_id')} run {row.get('run_id')}")
53
+
54
+ def _created_at(workspace: Path) -> str:
55
+ meta_path = workspace / "workspace.json"
56
+ if meta_path.is_file():
57
+ value = json.loads(meta_path.read_text(encoding="utf-8")).get("created_at_utc")
58
+ if isinstance(value, str):
59
+ return value
60
+ return workspace.name
61
+
62
+ return sorted(candidates, key=_created_at)
63
+
64
+
65
+ def build_recorded_fixture(
66
+ candidate_repo: str | Path,
67
+ eval_root: str | Path,
68
+ evidence_jsonl: str | Path,
69
+ *,
70
+ case_ids: list[str] | None = None,
71
+ run_id: str | None = None,
72
+ ) -> RecordedWorker:
73
+ repo = _load_candidate(candidate_repo)
74
+ eval_root = Path(eval_root).resolve()
75
+ rows = _load_jsonl(Path(evidence_jsonl).resolve())
76
+ if run_id is not None:
77
+ rows = [row for row in rows if row.get("run_id") == run_id]
78
+ if case_ids:
79
+ wanted = set(case_ids)
80
+ rows = [row for row in rows if row.get("case_id") in wanted]
81
+ if not rows:
82
+ raise SystemExit("no evidence rows selected for fixture")
83
+
84
+ from pdl_taskmaster.runtime.session_engine import SessionEngine
85
+
86
+ builder = RecordedFixtureBuilder()
87
+ for row in rows:
88
+ workspaces = _resolve_workspaces(eval_root, row)
89
+ recorded_calls = _ordered_recorded_calls(workspaces)
90
+ call_index = 0
91
+
92
+ def model_call(request: Any, _recorded=recorded_calls) -> str:
93
+ nonlocal call_index
94
+ if call_index >= len(_recorded):
95
+ raise SystemExit(
96
+ f"fixture replay exhausted for case {row.get('case_id')} "
97
+ f"at operation {request.operation}"
98
+ )
99
+ recorded = _recorded[call_index]
100
+ call_index += 1
101
+ if recorded["operation"] != request.operation:
102
+ raise SystemExit(
103
+ f"fixture replay order mismatch: expected {recorded['operation']} "
104
+ f"got {request.operation} for case {row.get('case_id')}"
105
+ )
106
+ builder.add(
107
+ request.operation,
108
+ hashlib.sha256(request.prompt.encode("utf-8")).hexdigest(),
109
+ recorded["response"],
110
+ metadata={"source": f"{row['case_id']}:{recorded['invocation_id']}"},
111
+ prompt_text=request.prompt,
112
+ )
113
+ return recorded["response"]
114
+
115
+ with tempfile.TemporaryDirectory(prefix="pdl-r2s-fixture-") as tmp:
116
+ engine = SessionEngine(
117
+ repo,
118
+ model_call,
119
+ higher_priority_constraints="Obey applicable provider/platform safety, privacy, permission, and tool constraints.",
120
+ available_execution_tools=[],
121
+ workspace_root=tmp,
122
+ )
123
+ for turn in row.get("turns", []):
124
+ engine.handle_user_message(turn.get("user", ""))
125
+ return builder.build()
126
+
127
+ def build_recorded_fixture_from_vendored(
128
+ candidate_repo: str | Path,
129
+ fixture_file: str | Path,
130
+ *,
131
+ case_ids: list[str] | None = None,
132
+ ) -> RecordedWorker:
133
+ """Build a recorded worker from the vendored standalone fixture.
134
+
135
+ Extraction adaptation: the qualified REPL recorded path previously read
136
+ recorded workspaces from an external evaluation repository. The standalone
137
+ harness vendors the exact recorded calls (operation, prompt hash, response)
138
+ so the published harness does not depend on that repository.
139
+ """
140
+ _load_candidate(candidate_repo)
141
+ value = json.loads(Path(fixture_file).read_text(encoding="utf-8"))
142
+ entries = value.get("entries") or []
143
+ if case_ids:
144
+ wanted = set(case_ids)
145
+ entries = [e for e in entries if (e.get("source") or "").split(":")[0] in wanted]
146
+ if not entries:
147
+ raise SystemExit("no vendored fixture entries selected")
148
+ builder = RecordedFixtureBuilder()
149
+ for entry in entries:
150
+ builder.add(
151
+ entry["operation"],
152
+ entry["prompt_sha256"],
153
+ entry["response"],
154
+ metadata={"source": entry.get("source", "vendored")},
155
+ prompt_text=entry.get("prompt_text"),
156
+ )
157
+ return builder.build()
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from pdl_taskmaster.providers.base import WorkerResult
6
+
7
+
8
+ class LiveStubWorker:
9
+ """Deterministic non-recorded worker for live-capability tests.
10
+
11
+ Label: DEVELOPMENT / LIVE DEMONSTRATION WORKER
12
+ NOT A QUALIFIED R2S MEASUREMENT CONDITION.
13
+
14
+ This adapter does not reference case IDs, evidence files, or recorded
15
+ turn sequences. It is intentionally generic so observation can be tested
16
+ against arbitrary new sessions.
17
+ """
18
+
19
+ def call(self, request) -> WorkerResult:
20
+ operation = request.operation
21
+ if operation == "INTERPRET_ACTIVATION":
22
+ payload = {"route": "APPLY_PROTOCOL"}
23
+ elif operation == "BOOTSTRAP_ANALYSIS":
24
+ payload = {
25
+ "kind": "ANALYSIS",
26
+ "task_summary": "The user requests a benign explanation of a topic in plain terms.",
27
+ "approach_notes": "",
28
+ "risk_notes": "",
29
+ "task_entities": [],
30
+ }
31
+ elif operation == "DRAFT_PROMPT":
32
+ payload = {
33
+ "kind": "PROMPT",
34
+ "prompt_body": "EXPLAIN the user's requested topic in plain terms.",
35
+ "approach_handoff": "NONE",
36
+ }
37
+ elif operation in {"INTERPRET_PROMPT_REVIEW", "INTERPRET_PLAN_REVIEW"}:
38
+ payload = {
39
+ "kind": "REVIEW_FACTS",
40
+ "task_change_dimensions": [],
41
+ "approach_change_dimensions": [],
42
+ "progression_requested": True,
43
+ }
44
+ elif operation == "DRAFT_PLAN":
45
+ payload = {
46
+ "neutral_plan_body": "1. Define the topic.\n2. Explain the key mechanisms.\n3. Summarize practical implications."
47
+ }
48
+ elif operation == "DRAFT_EXECUTE":
49
+ payload = {
50
+ "kind": "RESULT",
51
+ "brief_body": "1. Confirm the file contract.\n2. Use the declared wire format.\n3. Execute and verify.",
52
+ "execution_entities": [],
53
+ }
54
+ elif operation == "EMIT_RESULT_IR":
55
+ payload = {
56
+ "result_ir": {
57
+ "files": [],
58
+ "reconciliation": [],
59
+ "open_defects": [],
60
+ }
61
+ }
62
+ elif operation == "EXECUTE":
63
+ payload = {"kind": "RESULT", "body": "Completed."}
64
+ else:
65
+ raise RuntimeError(f"unhandled live stub operation: {operation}")
66
+ return WorkerResult(json.dumps(payload, ensure_ascii=False), {"worker": "live-stub", "mode": "live-demonstration"})
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+ import hashlib
6
+
7
+ from pdl_taskmaster.providers.base import WorkerResult
8
+
9
+
10
+ class ReplayMissError(RuntimeError):
11
+ pass
12
+
13
+
14
+ class AmbiguousFixtureError(RuntimeError):
15
+ pass
16
+
17
+
18
+ @dataclass
19
+ class RecordedEntry:
20
+ response: str
21
+ metadata: dict[str, Any] = field(default_factory=dict)
22
+
23
+
24
+ class RecordedFixtureBuilder:
25
+ def __init__(self) -> None:
26
+ self.entries: dict[tuple[str, str], list[RecordedEntry]] = {}
27
+ self.order: list[tuple[str, str]] = []
28
+ self.prompt_examples: dict[tuple[str, str], str] = {}
29
+
30
+ def add(
31
+ self,
32
+ operation: str,
33
+ prompt_sha256: str,
34
+ response: str,
35
+ metadata: dict[str, Any] | None = None,
36
+ *,
37
+ source: str = "fixture",
38
+ prompt_text: str | None = None,
39
+ ) -> None:
40
+ key = (operation, prompt_sha256)
41
+ self.entries.setdefault(key, []).append(
42
+ RecordedEntry(response, {"source": source, **(metadata or {})})
43
+ )
44
+ if prompt_text is not None:
45
+ self.prompt_examples.setdefault(key, prompt_text)
46
+ if key not in self.order:
47
+ self.order.append(key)
48
+
49
+ def build(self, selected_fixture_id: str | None = None) -> "RecordedWorker":
50
+ mapping: dict[tuple[str, str], RecordedEntry] = {}
51
+ for key, entries in self.entries.items():
52
+ unique = {entry.response: entry for entry in entries}
53
+ if len(unique) == 1:
54
+ mapping[key] = next(iter(unique.values()))
55
+ continue
56
+ if selected_fixture_id is None:
57
+ raise AmbiguousFixtureError(
58
+ f"ambiguous replay mapping for {key[0]} prompt={key[1][:12]}; "
59
+ "provide an explicit fixture/run ID"
60
+ )
61
+ selected = [
62
+ entry
63
+ for entry in entries
64
+ if entry.metadata.get("source") == selected_fixture_id
65
+ ]
66
+ if len(selected) != 1:
67
+ raise AmbiguousFixtureError(
68
+ f"fixture ID {selected_fixture_id!r} does not resolve exactly one response "
69
+ f"for {key[0]} prompt={key[1][:12]}"
70
+ )
71
+ mapping[key] = selected[0]
72
+ return RecordedWorker(mapping, list(self.order), dict(self.prompt_examples), dict(self.entries))
73
+
74
+
75
+ class RecordedWorker:
76
+ """Deterministic replay worker.
77
+
78
+ This is a qualification/test implementation of WorkerAdapter, not the
79
+ general-purpose worker contract.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ mapping: dict[tuple[str, str], RecordedEntry],
85
+ order: list[tuple[str, str]] | None = None,
86
+ prompt_examples: dict[tuple[str, str], str] | None = None,
87
+ entries: dict[tuple[str, str], list[RecordedEntry]] | None = None,
88
+ ):
89
+ self.mapping = mapping
90
+ self.order = list(order or [])
91
+ self.prompt_examples = prompt_examples or {}
92
+ self.entries = entries or {}
93
+
94
+ def call(self, request: Any) -> WorkerResult:
95
+ key = (request.operation, hashlib.sha256(request.prompt.encode("utf-8")).hexdigest())
96
+ entry = self.mapping.get(key)
97
+ if entry is None:
98
+ raise ReplayMissError(
99
+ f"no recorded response for operation={request.operation} prompt_sha256={key[1][:12]}"
100
+ )
101
+ return WorkerResult(entry.response, dict(entry.metadata))
102
+
103
+ def to_manifest(self) -> list[dict[str, str]]:
104
+ rows: list[dict[str, str]] = []
105
+ for key, entry in self.mapping.items():
106
+ operation, prompt_sha256 = key
107
+ source = str(entry.metadata.get("source", "unknown"))
108
+ fixture_id = hashlib.sha256(
109
+ f"{source}|{operation}|{prompt_sha256}".encode("utf-8")
110
+ ).hexdigest()[:16]
111
+ rows.append(
112
+ {
113
+ "source_evidence_run": source,
114
+ "operation": operation,
115
+ "prompt_sha256": prompt_sha256,
116
+ "raw_response_sha256": hashlib.sha256(entry.response.encode("utf-8")).hexdigest(),
117
+ "fixture_id": fixture_id,
118
+ }
119
+ )
120
+ return rows
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561.
File without changes