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,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
|
pdl_taskmaster/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561.
|
|
File without changes
|