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,1168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import tempfile
|
|
10
|
+
import hashlib
|
|
11
|
+
|
|
12
|
+
from pdl_taskmaster.controller.mechanical_controller import (
|
|
13
|
+
AtomicJsonStore,
|
|
14
|
+
ControllerError,
|
|
15
|
+
Intent,
|
|
16
|
+
MechanicalController,
|
|
17
|
+
MemoryAtomicJsonStore,
|
|
18
|
+
NextAction,
|
|
19
|
+
ProtocolState,
|
|
20
|
+
ReviewDecision,
|
|
21
|
+
Stage,
|
|
22
|
+
Transition,
|
|
23
|
+
)
|
|
24
|
+
from pdl_taskmaster.runtime.operation_bridge import ActivationRoute, ModelRequest, OperationBridge, WireError
|
|
25
|
+
from pdl_taskmaster.runtime.quarantine import compile_bootstrap_output
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _norm(text: str) -> str:
|
|
29
|
+
"""Whitespace-normalized form for verbatim entity matching: collapses all
|
|
30
|
+
whitespace runs to single spaces so formatting variance does not cause
|
|
31
|
+
false entity-miss retries (the 4B check is about content, not layout)."""
|
|
32
|
+
return " ".join((text or "").split())
|
|
33
|
+
from pdl_taskmaster.runtime.workspace import MemoryWorkspaceRun, WorkspaceError, WorkspaceRun
|
|
34
|
+
from pdl_taskmaster.runtime import presentation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
ModelCall = Callable[[ModelRequest], str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_EXPLICIT_INVOCATION = re.compile(
|
|
41
|
+
r"^\s*(?:(?:please\s+)?(?:(?:could|can|would)\s+you\s+)?(?:use|apply|invoke|run)\s+)?"
|
|
42
|
+
r"(?:\$confirm-with-pseudocode|\[\$confirm-with-pseudocode\]\([^)]+\))"
|
|
43
|
+
r"\s*(?:(?:to)\b|[.:,;-])?\s*(?P<body>.*)$",
|
|
44
|
+
re.IGNORECASE | re.DOTALL,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class InvocationObservation:
|
|
50
|
+
explicit: bool
|
|
51
|
+
substantive_request: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def observe_invocation(user_message: str) -> InvocationObservation:
|
|
55
|
+
"""Recognize host-visible leading skill invocation syntax.
|
|
56
|
+
|
|
57
|
+
Only the leading control wrapper is removed. Mentions inside the substantive
|
|
58
|
+
request remain data, including tasks whose subject is this protocol.
|
|
59
|
+
"""
|
|
60
|
+
match = _EXPLICIT_INVOCATION.match(user_message)
|
|
61
|
+
if not match:
|
|
62
|
+
return InvocationObservation(False, user_message.strip())
|
|
63
|
+
return InvocationObservation(True, match.group("body").strip())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class CallTrace:
|
|
68
|
+
operation: str
|
|
69
|
+
projection_manifest: dict[str, Any]
|
|
70
|
+
model_text: str
|
|
71
|
+
workspace_stage: str
|
|
72
|
+
workspace_invocation_id: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class EngineResponse:
|
|
77
|
+
text: str | None
|
|
78
|
+
traces: list[CallTrace] = field(default_factory=list)
|
|
79
|
+
bypass: bool = False
|
|
80
|
+
closed: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SessionEngine:
|
|
84
|
+
"""Condition C session orchestrator.
|
|
85
|
+
|
|
86
|
+
Control flow is owned by MechanicalController. Context flow and durable
|
|
87
|
+
stage-to-stage handoff are owned by WorkspaceRun. Semantic worker calls are
|
|
88
|
+
stateless and receive only the operation projection compiled from the
|
|
89
|
+
materialized stage inputs plus applicable Standard clauses.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
repo_root: str | Path,
|
|
95
|
+
model_call: ModelCall,
|
|
96
|
+
*,
|
|
97
|
+
higher_priority_constraints: Any = None,
|
|
98
|
+
available_execution_tools: Any = None,
|
|
99
|
+
workspace_root: str | Path | None = None,
|
|
100
|
+
render_compact: bool = False,
|
|
101
|
+
):
|
|
102
|
+
self.repo_root = Path(repo_root)
|
|
103
|
+
self.model_call = model_call
|
|
104
|
+
self.higher_priority_constraints = higher_priority_constraints
|
|
105
|
+
self.available_execution_tools = available_execution_tools
|
|
106
|
+
self.bridge = OperationBridge(self.repo_root, render_compact=render_compact)
|
|
107
|
+
self.controller: Optional[MechanicalController] = None
|
|
108
|
+
self.workspace: Optional[WorkspaceRun] = None
|
|
109
|
+
# Protocol v2: semantic-bootstrap containment (structural, non-optional).
|
|
110
|
+
# Raw untrusted content is read by BOOTSTRAP_ANALYSIS only; every compile
|
|
111
|
+
# operation receives the sanitized compiled analysis. Cache is keyed on
|
|
112
|
+
# the raw source so repeated sources bootstrap once per session.
|
|
113
|
+
self._bootstrap_cache: dict[tuple[str, str | None], str] = {}
|
|
114
|
+
self._task_entities_cache: dict[tuple[str, str | None], tuple[str, ...]] = {}
|
|
115
|
+
# S4: confirmed deliverable carried from the prior turn (chaining);
|
|
116
|
+
# None for first turns and legacy single-turn workspaces.
|
|
117
|
+
self._previous_deliverable: str | None = None
|
|
118
|
+
self._active_task_entities: tuple[str, ...] = ()
|
|
119
|
+
if workspace_root is None:
|
|
120
|
+
self.workspace_root = Path(tempfile.mkdtemp(prefix="pdl-c0-workspaces-"))
|
|
121
|
+
else:
|
|
122
|
+
self.workspace_root = Path(workspace_root)
|
|
123
|
+
self.workspace_root.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def restore(
|
|
127
|
+
cls,
|
|
128
|
+
repo_root: str | Path,
|
|
129
|
+
model_call: ModelCall,
|
|
130
|
+
workspace_path: str | Path,
|
|
131
|
+
*,
|
|
132
|
+
higher_priority_constraints: Any = None,
|
|
133
|
+
available_execution_tools: Any = None,
|
|
134
|
+
render_compact: bool = False,
|
|
135
|
+
) -> "SessionEngine":
|
|
136
|
+
workspace_path = Path(workspace_path)
|
|
137
|
+
engine = cls(
|
|
138
|
+
repo_root,
|
|
139
|
+
model_call,
|
|
140
|
+
higher_priority_constraints=higher_priority_constraints,
|
|
141
|
+
available_execution_tools=available_execution_tools,
|
|
142
|
+
workspace_root=workspace_path.parent,
|
|
143
|
+
render_compact=render_compact,
|
|
144
|
+
)
|
|
145
|
+
workspace = MemoryWorkspaceRun.open(repo_root, workspace_path)
|
|
146
|
+
# Pointer may sit on a turn whose controller never committed (e.g. a
|
|
147
|
+
# turn opened by S4 chaining and then interrupted before any model
|
|
148
|
+
# call). Fall back to the most recent turn with a committed,
|
|
149
|
+
# instance-matching state before declaring the session unrestorable.
|
|
150
|
+
candidates: list[tuple[str | None, Path, str | None]] = []
|
|
151
|
+
if workspace.turn_id is not None:
|
|
152
|
+
ordered_ids = [workspace.turn_id]
|
|
153
|
+
turns_dir = workspace.path / "turns"
|
|
154
|
+
if turns_dir.is_dir():
|
|
155
|
+
ordered_ids.extend(
|
|
156
|
+
d.name for d in sorted(turns_dir.iterdir(), reverse=True)
|
|
157
|
+
if d.is_dir() and re.match(r"^turn_\d+$", d.name) and d.name != workspace.turn_id
|
|
158
|
+
)
|
|
159
|
+
for turn_id in ordered_ids:
|
|
160
|
+
turn_meta = workspace.read_turn_status(turn_id)
|
|
161
|
+
candidates.append((
|
|
162
|
+
turn_id,
|
|
163
|
+
workspace.path / "turns" / turn_id / "state" / "controller-state.json",
|
|
164
|
+
turn_meta.get("protocol_instance_id") or workspace.protocol_instance_id,
|
|
165
|
+
))
|
|
166
|
+
else:
|
|
167
|
+
if workspace.protocol_instance_id is None:
|
|
168
|
+
raise WorkspaceError("restorable_protocol_state_missing")
|
|
169
|
+
candidates.append((None, workspace.controller_state_path, workspace.protocol_instance_id))
|
|
170
|
+
restore_state_path: Path | None = None
|
|
171
|
+
for turn_id, candidate_path, expected_instance in candidates:
|
|
172
|
+
if not candidate_path.is_file():
|
|
173
|
+
continue
|
|
174
|
+
candidate_state = json.loads(candidate_path.read_text(encoding="utf-8"))
|
|
175
|
+
if candidate_state.get("instance_id") == expected_instance:
|
|
176
|
+
restore_state_path = candidate_path
|
|
177
|
+
if turn_id is not None and turn_id != workspace.turn_id:
|
|
178
|
+
workspace.metadata["turn_id"] = turn_id
|
|
179
|
+
workspace._atomic_write(
|
|
180
|
+
workspace.metadata_path,
|
|
181
|
+
json.dumps(workspace.metadata, ensure_ascii=False, indent=2) + "\n",
|
|
182
|
+
)
|
|
183
|
+
workspace.turn_id = turn_id
|
|
184
|
+
break
|
|
185
|
+
if restore_state_path is None:
|
|
186
|
+
raise WorkspaceError("restorable_protocol_state_missing")
|
|
187
|
+
store = (
|
|
188
|
+
MemoryAtomicJsonStore(restore_state_path)
|
|
189
|
+
if isinstance(workspace, MemoryWorkspaceRun)
|
|
190
|
+
else AtomicJsonStore(restore_state_path)
|
|
191
|
+
)
|
|
192
|
+
state = store.load()
|
|
193
|
+
if state.instance_id != workspace.protocol_instance_id and workspace.turn_id is None:
|
|
194
|
+
raise WorkspaceError("restore_instance_binding")
|
|
195
|
+
engine.workspace = workspace
|
|
196
|
+
engine.controller = MechanicalController(state, store)
|
|
197
|
+
# Cross-epoch deliverable chaining: repopulate the prior confirmed
|
|
198
|
+
# deliverable on restore. The S4 turn-chaining path sets this field
|
|
199
|
+
# only when a new user message opens a turn in the same process; a
|
|
200
|
+
# resumed epoch (e.g. /confirm in a later REPL invocation) must
|
|
201
|
+
# re-chain it so the execution phase still receives the byte-exact
|
|
202
|
+
# prior deliverable in REQUIRED_TASK_INPUTS. The active turn's status
|
|
203
|
+
# is ACTIVE at restore time, so previous_deliverable() correctly
|
|
204
|
+
# returns the last CLOSED_SUCCESS turn before it.
|
|
205
|
+
engine._previous_deliverable = workspace.previous_deliverable()
|
|
206
|
+
workspace.append_event("SESSION_RESTORED", {"instance_id": state.instance_id})
|
|
207
|
+
return engine
|
|
208
|
+
|
|
209
|
+
def _new_workspace(self) -> WorkspaceRun:
|
|
210
|
+
# ADR-0011 / ADR-0008: software-defined in-memory VFS workspace run.
|
|
211
|
+
# Fast, unjournaled writes bypass Windows NTFS fsync latency.
|
|
212
|
+
# Legacy flat workspaces remain supported for direct WorkspaceRun.create callers.
|
|
213
|
+
return MemoryWorkspaceRun.create(self.repo_root, self.workspace_root, turn_id="turn_001")
|
|
214
|
+
|
|
215
|
+
def _bind_new_controller(self, workspace: WorkspaceRun) -> MechanicalController:
|
|
216
|
+
state = ProtocolState.new()
|
|
217
|
+
workspace.bind_protocol(state.instance_id)
|
|
218
|
+
store = (
|
|
219
|
+
MemoryAtomicJsonStore(workspace.controller_state_path)
|
|
220
|
+
if isinstance(workspace, MemoryWorkspaceRun)
|
|
221
|
+
else AtomicJsonStore(workspace.controller_state_path)
|
|
222
|
+
)
|
|
223
|
+
controller = MechanicalController(state, store)
|
|
224
|
+
workspace.publish_approach_sources([])
|
|
225
|
+
return controller
|
|
226
|
+
|
|
227
|
+
def _invoke(self, request: ModelRequest, traces: list[CallTrace]) -> str:
|
|
228
|
+
model_text = self.model_call(request)
|
|
229
|
+
self.workspace.record_model_output(request.workspace_invocation, model_text)
|
|
230
|
+
traces.append(
|
|
231
|
+
CallTrace(
|
|
232
|
+
request.operation,
|
|
233
|
+
request.manifest,
|
|
234
|
+
model_text,
|
|
235
|
+
request.workspace_invocation.stage,
|
|
236
|
+
request.workspace_invocation.invocation_id,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
return model_text
|
|
240
|
+
|
|
241
|
+
def _call(
|
|
242
|
+
self,
|
|
243
|
+
operation: str,
|
|
244
|
+
values: dict[str, Any],
|
|
245
|
+
traces: list[CallTrace],
|
|
246
|
+
parser: Callable[[str], Any] | None = None,
|
|
247
|
+
) -> Any:
|
|
248
|
+
"""Invoke one operation. With a parser, retry ONCE on WireError with an
|
|
249
|
+
operator correction so a sampling glitch (invalid JSON, dropped field)
|
|
250
|
+
costs one extra call instead of fatally failing the session."""
|
|
251
|
+
if self.workspace is None:
|
|
252
|
+
raise WorkspaceError("workspace_not_initialized")
|
|
253
|
+
request = self.bridge.request(
|
|
254
|
+
operation,
|
|
255
|
+
values,
|
|
256
|
+
workspace=self.workspace,
|
|
257
|
+
higher_priority_constraints=self.higher_priority_constraints,
|
|
258
|
+
)
|
|
259
|
+
model_text = self._invoke(request, traces)
|
|
260
|
+
if parser is None:
|
|
261
|
+
return model_text
|
|
262
|
+
try:
|
|
263
|
+
return parser(model_text)
|
|
264
|
+
except WireError as first_error:
|
|
265
|
+
feedback = getattr(first_error, "operator_feedback", None)
|
|
266
|
+
if feedback:
|
|
267
|
+
correction = (
|
|
268
|
+
f"OPERATOR CORRECTION: {feedback}. "
|
|
269
|
+
"Emit exactly one conforming JSON object matching the declared schema."
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
correction = (
|
|
273
|
+
"OPERATOR CORRECTION: the previous response failed host-side validation "
|
|
274
|
+
f"(reason: {first_error}). Emit exactly one JSON object that conforms to the "
|
|
275
|
+
"declared output_schema for this operation, with no prose or code fences around it."
|
|
276
|
+
)
|
|
277
|
+
retry_request = self.bridge.request(
|
|
278
|
+
operation,
|
|
279
|
+
values,
|
|
280
|
+
workspace=self.workspace,
|
|
281
|
+
higher_priority_constraints=self.higher_priority_constraints,
|
|
282
|
+
operator_correction=correction,
|
|
283
|
+
)
|
|
284
|
+
retry_text = self._invoke(retry_request, traces)
|
|
285
|
+
try:
|
|
286
|
+
return parser(retry_text)
|
|
287
|
+
except WireError:
|
|
288
|
+
raise first_error from None
|
|
289
|
+
|
|
290
|
+
def _publish_prompt(self) -> None:
|
|
291
|
+
assert self.controller is not None and self.workspace is not None
|
|
292
|
+
prompt = self.controller.state.current_prompt
|
|
293
|
+
assert prompt is not None
|
|
294
|
+
self.workspace.publish_artifact("prompt", prompt.artifact_id, prompt.body, confirmed=prompt.confirmed)
|
|
295
|
+
self.workspace.publish_approach_sources(list(self.controller.state.approach_sources))
|
|
296
|
+
|
|
297
|
+
def _publish_plan(self) -> None:
|
|
298
|
+
assert self.controller is not None and self.workspace is not None
|
|
299
|
+
plan = self.controller.state.current_plan
|
|
300
|
+
assert plan is not None
|
|
301
|
+
prompt = self.controller.state.current_prompt
|
|
302
|
+
assert prompt is not None
|
|
303
|
+
self.workspace.publish_artifact(
|
|
304
|
+
"plan",
|
|
305
|
+
plan.artifact_id,
|
|
306
|
+
plan.body,
|
|
307
|
+
confirmed=plan.confirmed,
|
|
308
|
+
source_prompt_id=plan.source_prompt_id,
|
|
309
|
+
confirmed_prompt_hash=hashlib.sha256(prompt.body.encode("utf-8")).hexdigest(),
|
|
310
|
+
)
|
|
311
|
+
self.workspace.publish_approach_sources(list(self.controller.state.approach_sources))
|
|
312
|
+
|
|
313
|
+
def _semantic_read(self, raw_text: str, traces: list[CallTrace]) -> str | None:
|
|
314
|
+
"""Protocol v2 structural containment: the ONLY operation that sees raw
|
|
315
|
+
untrusted content. Returns the sanitized compiled analysis for compile
|
|
316
|
+
operations, or None when the bootstrap blocks under higher priority.
|
|
317
|
+
"""
|
|
318
|
+
cache_key = (raw_text, self._previous_deliverable)
|
|
319
|
+
if cache_key in self._bootstrap_cache:
|
|
320
|
+
return self._bootstrap_cache[cache_key]
|
|
321
|
+
bootstrap_values: dict[str, Any] = {
|
|
322
|
+
"HOST_PROTOCOL_STATE": "SEMANTIC_READ",
|
|
323
|
+
"RAW_UNTRUSTED_CONTENT": raw_text,
|
|
324
|
+
}
|
|
325
|
+
if self._previous_deliverable:
|
|
326
|
+
# S4: the prior turn's confirmed deliverable is host-published,
|
|
327
|
+
# gate-passed content (never raw untrusted input). It is compiled
|
|
328
|
+
# as inactive background context for the new turn.
|
|
329
|
+
bootstrap_values["PREVIOUS_DELIVERABLE"] = self._previous_deliverable
|
|
330
|
+
outcome = self._call(
|
|
331
|
+
"BOOTSTRAP_ANALYSIS",
|
|
332
|
+
bootstrap_values,
|
|
333
|
+
traces,
|
|
334
|
+
parser=self.bridge.parse_bootstrap_analysis,
|
|
335
|
+
)
|
|
336
|
+
if outcome["kind"] == "BLOCKED_BY_HIGHER_PRIORITY":
|
|
337
|
+
self._bootstrap_cache[cache_key] = ""
|
|
338
|
+
return None
|
|
339
|
+
import hashlib
|
|
340
|
+
|
|
341
|
+
compiled, _meta = compile_bootstrap_output(raw_text, outcome["task_summary"])
|
|
342
|
+
# Containment-boundary durability: a lazy semantic read that classifies
|
|
343
|
+
# substantive raw content as instruction-free loses the entire task
|
|
344
|
+
# upstream of every gate. Detect and retry once before compiling.
|
|
345
|
+
summary_lowers = (outcome["task_summary"] or "").lower()
|
|
346
|
+
if len(raw_text) > 500 and (
|
|
347
|
+
"no operative instructions" in summary_lowers
|
|
348
|
+
or "no substantive" in summary_lowers
|
|
349
|
+
or "no requested" in summary_lowers
|
|
350
|
+
or len((outcome["task_summary"] or "").strip()) < 40
|
|
351
|
+
):
|
|
352
|
+
self.workspace.append_event(
|
|
353
|
+
"BOOTSTRAP_DEGENERATE_READ_RETRY",
|
|
354
|
+
{"raw_chars": len(raw_text), "summary_chars": len(outcome["task_summary"] or "")},
|
|
355
|
+
) if self.workspace is not None else None
|
|
356
|
+
outcome = self._call(
|
|
357
|
+
"BOOTSTRAP_ANALYSIS",
|
|
358
|
+
bootstrap_values,
|
|
359
|
+
traces,
|
|
360
|
+
parser=self.bridge.parse_bootstrap_analysis,
|
|
361
|
+
)
|
|
362
|
+
if outcome["kind"] == "BLOCKED_BY_HIGHER_PRIORITY":
|
|
363
|
+
self._bootstrap_cache[cache_key] = ""
|
|
364
|
+
return None
|
|
365
|
+
compiled, _meta = compile_bootstrap_output(raw_text, outcome["task_summary"])
|
|
366
|
+
# Mechanical entity containment: a task entity is forwarded downstream
|
|
367
|
+
# ONLY if it is a verbatim substring of the SANITIZED task summary.
|
|
368
|
+
# Hostile tokens (canaries, exploit directives) are replaced by the
|
|
369
|
+
# sanitizer, so a hostile entity can never pass this filter -- the
|
|
370
|
+
# verbatim-preservation channel inherits the compile tier's redaction.
|
|
371
|
+
raw_entities = outcome.get("task_entities") or []
|
|
372
|
+
entities = tuple(
|
|
373
|
+
e for e in raw_entities
|
|
374
|
+
if isinstance(e, str) and e.strip() and e in compiled
|
|
375
|
+
)
|
|
376
|
+
dropped = [e for e in raw_entities if e not in entities]
|
|
377
|
+
if dropped and self.workspace is not None:
|
|
378
|
+
self.workspace.append_event("TASK_ENTITY_DROPPED_UNSAFE", {"count": len(dropped)})
|
|
379
|
+
self._task_entities_cache[cache_key] = entities
|
|
380
|
+
# In Protocol v2 out-of-band field isolation: approach_notes carries TASK-02
|
|
381
|
+
# procedural guidance for planning. risk_notes is quarantined threat data
|
|
382
|
+
# retained in telemetry/traces, not leaked into compile contexts.
|
|
383
|
+
approach = outcome.get("approach_notes", "")
|
|
384
|
+
notes, _ = compile_bootstrap_output(raw_text, approach) if approach else ("", {})
|
|
385
|
+
if "risk_notes" in outcome and isinstance(outcome["risk_notes"], str):
|
|
386
|
+
sanitized_risk, _ = compile_bootstrap_output(raw_text, outcome["risk_notes"])
|
|
387
|
+
outcome["risk_notes"] = sanitized_risk
|
|
388
|
+
document = (
|
|
389
|
+
f"TASK SUMMARY (compiled semantic analysis; untrusted literals redacted):\n{compiled}\n"
|
|
390
|
+
f"APPROACH/RISK NOTES:\n{notes}"
|
|
391
|
+
)
|
|
392
|
+
if entities:
|
|
393
|
+
document += (
|
|
394
|
+
"\nOPERATIVE TASK ENTITIES (copy each EXACTLY, character-for-character, into the "
|
|
395
|
+
"task_entities array AND reproduce each verbatim inside the prompt body):\n"
|
|
396
|
+
+ "\n".join(f"- {e}" for e in entities)
|
|
397
|
+
)
|
|
398
|
+
self._bootstrap_cache[cache_key] = document
|
|
399
|
+
return document
|
|
400
|
+
|
|
401
|
+
def _compile_context(self, raw_text: str, traces: list[CallTrace]) -> str:
|
|
402
|
+
"""Route raw content through the semantic read; compile ops never see raw."""
|
|
403
|
+
document = self._semantic_read(raw_text, traces)
|
|
404
|
+
if document is None:
|
|
405
|
+
return "[CONTENT BLOCKED BY HIGHER-PRIORITY CONSTRAINTS]"
|
|
406
|
+
return document
|
|
407
|
+
|
|
408
|
+
def _entity_coverage_missing(self, prompt_body: str, entities: tuple[str, ...]) -> list[str]:
|
|
409
|
+
"""Mechanical source-coverage check: every forwarded task entity must
|
|
410
|
+
appear verbatim in the prompt IR. Host-side string presence only -- no
|
|
411
|
+
model judgment, no generation constraint."""
|
|
412
|
+
return [e for e in entities if e not in prompt_body]
|
|
413
|
+
|
|
414
|
+
def _enforce_entity_coverage(
|
|
415
|
+
self,
|
|
416
|
+
draft_fn,
|
|
417
|
+
context: dict[str, Any],
|
|
418
|
+
parser,
|
|
419
|
+
entities: tuple[str, ...],
|
|
420
|
+
traces: list[CallTrace],
|
|
421
|
+
phase: str,
|
|
422
|
+
) -> Any:
|
|
423
|
+
"""Call the draft op, then mechanically verify task-entity coverage of
|
|
424
|
+
the prompt body. On a miss, retry once with an operator correction
|
|
425
|
+
appended outside the projection document. Persistent misses are
|
|
426
|
+
published with a workspace event (utility-first: measurable, not
|
|
427
|
+
fatal)."""
|
|
428
|
+
outcome = draft_fn(context, traces, parser=parser)
|
|
429
|
+
if getattr(outcome, "kind", "") == "TASK_BLOCKED_BY_HIGHER_PRIORITY":
|
|
430
|
+
return outcome
|
|
431
|
+
missing = self._entity_coverage_missing(outcome.prompt_body or "", entities)
|
|
432
|
+
if not missing:
|
|
433
|
+
return outcome
|
|
434
|
+
if self.workspace is not None:
|
|
435
|
+
self.workspace.append_event("TASK_ENTITY_COVERAGE_RETRY", {"missing": len(missing)})
|
|
436
|
+
corrected_context = dict(context)
|
|
437
|
+
corrected_context["SUBSTANTIVE_REQUEST"] = (
|
|
438
|
+
context["SUBSTANTIVE_REQUEST"]
|
|
439
|
+
+ "\n\nOPERATOR CORRECTION (host-side mechanical check): the following task entities are "
|
|
440
|
+
"missing from the prompt body and MUST appear verbatim, character-for-character: "
|
|
441
|
+
+ "; ".join(missing)
|
|
442
|
+
)
|
|
443
|
+
outcome = draft_fn(corrected_context, traces, parser=parser)
|
|
444
|
+
if getattr(outcome, "kind", "") == "TASK_BLOCKED_BY_HIGHER_PRIORITY":
|
|
445
|
+
return outcome
|
|
446
|
+
missing = self._entity_coverage_missing(outcome.prompt_body or "", entities)
|
|
447
|
+
if missing and self.workspace is not None:
|
|
448
|
+
self.workspace.append_event(
|
|
449
|
+
"TASK_ENTITY_COVERAGE_MISSING",
|
|
450
|
+
{"entities": list(missing), "phase": phase},
|
|
451
|
+
)
|
|
452
|
+
return outcome
|
|
453
|
+
|
|
454
|
+
def _draft_initial_prompt(
|
|
455
|
+
self,
|
|
456
|
+
substantive_request: str,
|
|
457
|
+
traces: list[CallTrace],
|
|
458
|
+
*,
|
|
459
|
+
protocol_state: str,
|
|
460
|
+
) -> EngineResponse:
|
|
461
|
+
assert self.workspace is not None
|
|
462
|
+
# Protocol v2: raw content is read by BOOTSTRAP_ANALYSIS only; the
|
|
463
|
+
# compile op receives the sanitized compiled analysis.
|
|
464
|
+
compiled = self._semantic_read(substantive_request, traces)
|
|
465
|
+
if compiled is None:
|
|
466
|
+
self.workspace.append_event("PROTOCOL_BLOCKED", {"phase": "bootstrap"})
|
|
467
|
+
return EngineResponse(presentation.cancelled(), traces, closed=True)
|
|
468
|
+
entities = self._task_entities_cache.get((substantive_request, self._previous_deliverable), ())
|
|
469
|
+
self._active_task_entities = entities
|
|
470
|
+
|
|
471
|
+
def _draft_call(ctx: dict[str, Any], tr: list[CallTrace], parser) -> Any:
|
|
472
|
+
return self._call("DRAFT_PROMPT", ctx, tr, parser=parser)
|
|
473
|
+
|
|
474
|
+
outcome = self._enforce_entity_coverage(
|
|
475
|
+
_draft_call,
|
|
476
|
+
{
|
|
477
|
+
"HOST_PROTOCOL_STATE": protocol_state,
|
|
478
|
+
"SUBSTANTIVE_REQUEST": compiled,
|
|
479
|
+
},
|
|
480
|
+
self.bridge.parse_prompt_draft,
|
|
481
|
+
entities,
|
|
482
|
+
traces,
|
|
483
|
+
phase="draft_prompt",
|
|
484
|
+
)
|
|
485
|
+
if outcome.kind == "TASK_BLOCKED_BY_HIGHER_PRIORITY":
|
|
486
|
+
self.workspace.append_event(
|
|
487
|
+
"PROTOCOL_BLOCKED",
|
|
488
|
+
{"phase": "prompt_draft", "blocking_basis": outcome.blocking_basis},
|
|
489
|
+
)
|
|
490
|
+
return EngineResponse(outcome.response, traces, closed=True)
|
|
491
|
+
assert outcome.prompt_body is not None
|
|
492
|
+
self.controller = self._bind_new_controller(self.workspace)
|
|
493
|
+
approach_source = substantive_request if outcome.approach_handoff == "CARRY_SOURCE_TO_PLAN" else None
|
|
494
|
+
self.controller.commit_initial_prompt(outcome.prompt_body, approach_source)
|
|
495
|
+
self._publish_prompt()
|
|
496
|
+
return EngineResponse(presentation.prompt_artifact(outcome.prompt_body), traces)
|
|
497
|
+
|
|
498
|
+
def _sync_review_edit(self) -> None:
|
|
499
|
+
assert self.controller is not None and self.workspace is not None
|
|
500
|
+
state = self.controller.state
|
|
501
|
+
if state.stage == Stage.PROMPT_REVIEW and state.current_prompt:
|
|
502
|
+
body = self.workspace.sync_unconfirmed_edit("prompt", state.current_prompt.artifact_id, state.current_prompt.body)
|
|
503
|
+
if body != state.current_prompt.body:
|
|
504
|
+
self.controller.replace_current_unconfirmed_body("prompt", body)
|
|
505
|
+
elif state.stage == Stage.PLAN_REVIEW and state.current_plan:
|
|
506
|
+
self.workspace.validate_confirmed_artifact(
|
|
507
|
+
"prompt", state.current_prompt.artifact_id, state.current_prompt.body # type: ignore[union-attr]
|
|
508
|
+
)
|
|
509
|
+
body = self.workspace.sync_unconfirmed_edit("plan", state.current_plan.artifact_id, state.current_plan.body)
|
|
510
|
+
if body != state.current_plan.body:
|
|
511
|
+
self.controller.replace_current_unconfirmed_body("plan", body)
|
|
512
|
+
|
|
513
|
+
def _activation(self, user_message: str, traces: list[CallTrace]) -> EngineResponse | None:
|
|
514
|
+
# S4 cross-turn chaining (ADR-0008 §4): when the SAME session's prior
|
|
515
|
+
# turn reached a terminal stage and the user issues a new command,
|
|
516
|
+
# continue in the same workspace under the next turn id. Only the prior
|
|
517
|
+
# turn's confirmed deliverable carries forward; drafts, rejected plans,
|
|
518
|
+
# and review dialogue are structurally unreachable in the new turn.
|
|
519
|
+
prior = self.workspace
|
|
520
|
+
if (
|
|
521
|
+
prior is not None
|
|
522
|
+
and prior.turn_id is not None
|
|
523
|
+
and self.controller is not None
|
|
524
|
+
and self.controller.state.stage in {Stage.CLOSED_SUCCESS, Stage.CLOSED_CANCELLED}
|
|
525
|
+
):
|
|
526
|
+
prior_status = prior.read_turn_status(prior.turn_id).get("status")
|
|
527
|
+
if prior_status == "ACTIVE":
|
|
528
|
+
prior.mark_turn_status(
|
|
529
|
+
"CLOSED_SUCCESS"
|
|
530
|
+
if self.controller.state.stage == Stage.CLOSED_SUCCESS
|
|
531
|
+
else "CLOSED_CANCELLED"
|
|
532
|
+
)
|
|
533
|
+
chained_deliverable = prior.previous_deliverable()
|
|
534
|
+
prior.start_turn(prior.next_turn_id())
|
|
535
|
+
self.workspace = prior
|
|
536
|
+
self._previous_deliverable = chained_deliverable
|
|
537
|
+
self.workspace.append_event(
|
|
538
|
+
"TURN_CHAINED",
|
|
539
|
+
{"turn_id": prior.turn_id, "previous_deliverable": chained_deliverable is not None},
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
self.workspace = self._new_workspace()
|
|
543
|
+
self._previous_deliverable = None
|
|
544
|
+
observation = observe_invocation(user_message)
|
|
545
|
+
if observation.explicit:
|
|
546
|
+
self.workspace.append_event(
|
|
547
|
+
"EXPLICIT_INVOCATION_OBSERVED",
|
|
548
|
+
{"substantive_request_present": bool(observation.substantive_request)},
|
|
549
|
+
)
|
|
550
|
+
return self._draft_initial_prompt(
|
|
551
|
+
observation.substantive_request,
|
|
552
|
+
traces,
|
|
553
|
+
protocol_state="ACTIVE_BY_EXPLICIT_INVOCATION",
|
|
554
|
+
)
|
|
555
|
+
decision = self._call(
|
|
556
|
+
"INTERPRET_ACTIVATION", {"RAW_USER_MESSAGE": user_message}, traces,
|
|
557
|
+
parser=self.bridge.parse_activation,
|
|
558
|
+
)
|
|
559
|
+
if decision.route == ActivationRoute.BLOCKED_BY_HIGHER_PRIORITY:
|
|
560
|
+
return EngineResponse(decision.response, traces, closed=True)
|
|
561
|
+
if decision.route == ActivationRoute.BYPASS:
|
|
562
|
+
return EngineResponse(None, traces, bypass=True)
|
|
563
|
+
if decision.route == ActivationRoute.PROTOCOL_DISCUSSION:
|
|
564
|
+
return EngineResponse(self._call(
|
|
565
|
+
"ANSWER_PROTOCOL_DISCUSSION",
|
|
566
|
+
{
|
|
567
|
+
"RAW_PROTOCOL_QUESTION": user_message,
|
|
568
|
+
"CURRENT_STAGE_CLASS": None,
|
|
569
|
+
"BOUND_REVIEW_SUBJECT_KIND": None,
|
|
570
|
+
"BOUND_REVIEW_SUBJECT_BODY": None,
|
|
571
|
+
},
|
|
572
|
+
traces,
|
|
573
|
+
parser=self.bridge.parse_protocol_discussion,
|
|
574
|
+
), traces)
|
|
575
|
+
return self._draft_initial_prompt(
|
|
576
|
+
user_message.strip(),
|
|
577
|
+
traces,
|
|
578
|
+
protocol_state="ACTIVE_BY_SEMANTIC_REQUEST",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def _draft_plan(self, transition: Transition, traces: list[CallTrace]) -> EngineResponse:
|
|
582
|
+
assert self.controller is not None and self.workspace is not None
|
|
583
|
+
prompt = self.controller.state.current_prompt
|
|
584
|
+
assert prompt is not None
|
|
585
|
+
self.workspace.validate_confirmed_artifact("prompt", prompt.artifact_id, prompt.body)
|
|
586
|
+
_, prompt_body = self.workspace.read_artifact("prompt")
|
|
587
|
+
carried_raw = self.workspace.read_approach_sources()
|
|
588
|
+
if carried_raw != self.controller.state.approach_sources:
|
|
589
|
+
raise WorkspaceError("approach_source_handoff")
|
|
590
|
+
carried = [self._compile_context(s, traces) for s in carried_raw]
|
|
591
|
+
body = self._call(
|
|
592
|
+
"DRAFT_PLAN",
|
|
593
|
+
{
|
|
594
|
+
"CONFIRMED_PROMPT_BODY": prompt_body,
|
|
595
|
+
"CARRIED_APPROACH_SOURCES": carried,
|
|
596
|
+
},
|
|
597
|
+
traces,
|
|
598
|
+
parser=self.bridge.parse_plan_body,
|
|
599
|
+
)
|
|
600
|
+
self.controller.commit_plan(body)
|
|
601
|
+
self._publish_plan()
|
|
602
|
+
return EngineResponse(presentation.plan_artifact(body), traces)
|
|
603
|
+
|
|
604
|
+
def _revise_prompt(self, transition: Transition, traces: list[CallTrace]) -> EngineResponse:
|
|
605
|
+
assert self.controller is not None and self.workspace is not None
|
|
606
|
+
prompt = self.controller.state.current_prompt
|
|
607
|
+
assert prompt is not None
|
|
608
|
+
meta, prompt_body = self.workspace.read_artifact("prompt")
|
|
609
|
+
if meta.get("artifact_id") != prompt.artifact_id or prompt_body != prompt.body:
|
|
610
|
+
raise WorkspaceError("prompt_revision_handoff")
|
|
611
|
+
change_id = transition.payload["change_id"]
|
|
612
|
+
had_plan = self.controller.state.current_plan is not None
|
|
613
|
+
try:
|
|
614
|
+
body = self._call(
|
|
615
|
+
"REVISE_PROMPT",
|
|
616
|
+
{
|
|
617
|
+
"CURRENT_PROMPT_BODY": prompt_body,
|
|
618
|
+
# Protocol v2: raw change source routed through the
|
|
619
|
+
# semantic read; compile op receives the compiled form.
|
|
620
|
+
"TASK_CHANGE_SOURCE": self._compile_context(
|
|
621
|
+
transition.payload["task_change_source"], traces
|
|
622
|
+
),
|
|
623
|
+
},
|
|
624
|
+
traces,
|
|
625
|
+
parser=self.bridge.parse_prompt_body,
|
|
626
|
+
)
|
|
627
|
+
self.controller.commit_prompt_revision(change_id, body)
|
|
628
|
+
# Mechanical coverage regression check on revisions: entities the
|
|
629
|
+
# draft carried must survive revision. Event-only in v1 (no loop).
|
|
630
|
+
missing = self._entity_coverage_missing(body, self._active_task_entities)
|
|
631
|
+
if missing and self.workspace is not None:
|
|
632
|
+
self.workspace.append_event(
|
|
633
|
+
"TASK_ENTITY_COVERAGE_MISSING",
|
|
634
|
+
{"entities": list(missing), "phase": "revise_prompt"},
|
|
635
|
+
)
|
|
636
|
+
except Exception:
|
|
637
|
+
self.controller.abort_pending_change(change_id)
|
|
638
|
+
raise
|
|
639
|
+
if had_plan:
|
|
640
|
+
self.workspace.invalidate_artifact("plan", "prompt_revision")
|
|
641
|
+
self._publish_prompt()
|
|
642
|
+
return EngineResponse(presentation.prompt_artifact(body), traces)
|
|
643
|
+
|
|
644
|
+
def _revise_plan(self, transition: Transition, traces: list[CallTrace]) -> EngineResponse:
|
|
645
|
+
assert self.controller is not None and self.workspace is not None
|
|
646
|
+
prompt = self.controller.state.current_prompt
|
|
647
|
+
plan = self.controller.state.current_plan
|
|
648
|
+
assert prompt is not None and plan is not None
|
|
649
|
+
self.workspace.validate_confirmed_artifact("prompt", prompt.artifact_id, prompt.body)
|
|
650
|
+
prompt_body = self.workspace.read_artifact("prompt")[1]
|
|
651
|
+
plan_meta, plan_body = self.workspace.read_artifact("plan")
|
|
652
|
+
if plan_meta.get("artifact_id") != plan.artifact_id or plan_body != plan.body:
|
|
653
|
+
raise WorkspaceError("plan_revision_handoff")
|
|
654
|
+
change_id = transition.payload["change_id"]
|
|
655
|
+
carried_raw = self.workspace.read_approach_sources()
|
|
656
|
+
if carried_raw != self.controller.state.approach_sources:
|
|
657
|
+
raise WorkspaceError("approach_source_handoff")
|
|
658
|
+
try:
|
|
659
|
+
body = self._call(
|
|
660
|
+
"REVISE_PLAN",
|
|
661
|
+
{
|
|
662
|
+
"CONFIRMED_PROMPT_BODY": prompt_body,
|
|
663
|
+
"CURRENT_PLAN_BODY": plan_body,
|
|
664
|
+
"CARRIED_APPROACH_SOURCES": [
|
|
665
|
+
*map(lambda s: self._compile_context(s, traces), carried_raw),
|
|
666
|
+
self._compile_context(transition.payload["approach_change_source"], traces),
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
traces,
|
|
670
|
+
parser=self.bridge.parse_plan_body,
|
|
671
|
+
)
|
|
672
|
+
self.controller.commit_plan_revision(change_id, body)
|
|
673
|
+
except Exception:
|
|
674
|
+
self.controller.abort_pending_change(change_id)
|
|
675
|
+
raise
|
|
676
|
+
self._publish_plan()
|
|
677
|
+
return EngineResponse(presentation.plan_artifact(body), traces)
|
|
678
|
+
|
|
679
|
+
def _execute(self, transition: Transition, traces: list[CallTrace]) -> EngineResponse:
|
|
680
|
+
assert self.controller is not None and self.workspace is not None
|
|
681
|
+
if not self.controller.can_execute():
|
|
682
|
+
raise ControllerError("execute_gate")
|
|
683
|
+
prompt = self.controller.state.current_prompt
|
|
684
|
+
plan = self.controller.state.current_plan
|
|
685
|
+
assert prompt is not None and plan is not None
|
|
686
|
+
self.workspace.validate_confirmed_artifact("prompt", prompt.artifact_id, prompt.body)
|
|
687
|
+
self.workspace.validate_confirmed_artifact("plan", plan.artifact_id, plan.body)
|
|
688
|
+
prompt_body = self.workspace.read_artifact("prompt")[1]
|
|
689
|
+
plan_body = self.workspace.read_artifact("plan")[1]
|
|
690
|
+
# Protocol v2: the supplied execution input is raw user content (in the
|
|
691
|
+
# adversarial battery it IS the untrusted block). Route it through the
|
|
692
|
+
# semantic read — compile ops never receive raw content in any symbol.
|
|
693
|
+
supplied_raw = transition.payload.get("execution_input_source")
|
|
694
|
+
supplied_compiled = self._compile_context(supplied_raw, traces) if supplied_raw else None
|
|
695
|
+
# Cross-epoch deliverable chaining (ADR-0008 §4 + ADR-0004): the prior
|
|
696
|
+
# epoch's confirmed deliverable is host-published, gate-passed content
|
|
697
|
+
# (see _semantic_read S4 note). Chain it byte-exact into the execution
|
|
698
|
+
# phase so continuation/revision epochs condition on the actual prior
|
|
699
|
+
# artifact instead of a pseudocode summary. First epochs have no prior
|
|
700
|
+
# deliverable and keep the previous null behavior.
|
|
701
|
+
required_task_inputs = self._previous_deliverable
|
|
702
|
+
# ADR-0009 / TRD-0003 (RS-09, RS-10): feature-gated Result IR mode.
|
|
703
|
+
# Adds structured result-decomposition instructions, chains the prior
|
|
704
|
+
# validated Result IR beside the deliverable, and mechanically
|
|
705
|
+
# validates the emitted IR (coverage + evidence resolution).
|
|
706
|
+
result_ir_mode = os.environ.get("PDLT_RESULT_IR") == "1"
|
|
707
|
+
from pdl_taskmaster.runtime.result_ir import (
|
|
708
|
+
arithmetic_checks,
|
|
709
|
+
derive_requirements,
|
|
710
|
+
entity_enforcement_misses,
|
|
711
|
+
extract_result_ir,
|
|
712
|
+
load_ir_from_deliverable,
|
|
713
|
+
parse_typed_entities,
|
|
714
|
+
render_execution_brief,
|
|
715
|
+
render_instructions,
|
|
716
|
+
render_prior_ir_section,
|
|
717
|
+
validate_result_ir,
|
|
718
|
+
)
|
|
719
|
+
requirements = derive_requirements(prompt_body) if result_ir_mode else []
|
|
720
|
+
execute_context = {
|
|
721
|
+
"CONFIRMED_PROMPT_BODY": prompt_body,
|
|
722
|
+
"CONFIRMED_PLAN_BODY": plan_body,
|
|
723
|
+
"REQUIRED_TASK_INPUTS": required_task_inputs,
|
|
724
|
+
"SUPPLIED_EXECUTION_INPUT_SOURCE": supplied_compiled,
|
|
725
|
+
"AVAILABLE_EXECUTION_TOOLS": self.available_execution_tools,
|
|
726
|
+
}
|
|
727
|
+
if result_ir_mode:
|
|
728
|
+
# TRD-0003: the IR channel rides inside the contract-whitelisted
|
|
729
|
+
# REQUIRED_TASK_INPUTS symbol (adding new symbols would violate the
|
|
730
|
+
# EXECUTION_CONTRACT allow-list). Value = instructions + numbered
|
|
731
|
+
# requirements + (when chained) the prior validated IR.
|
|
732
|
+
evidence_paths = ["execution://body"]
|
|
733
|
+
if self._previous_deliverable and self.workspace.closed_turns():
|
|
734
|
+
closed = [t for t in self.workspace.closed_turns() if t.get("status") == "CLOSED_SUCCESS"]
|
|
735
|
+
if closed:
|
|
736
|
+
evidence_paths.append(
|
|
737
|
+
f"turns/{closed[-1]['turn_id']}/stages/50_execution/output/current.md"
|
|
738
|
+
)
|
|
739
|
+
ir_channel = render_instructions(
|
|
740
|
+
requirements, repo_root=self.repo_root, evidence_paths=evidence_paths
|
|
741
|
+
)
|
|
742
|
+
prior_ir = load_ir_from_deliverable(self._previous_deliverable)
|
|
743
|
+
if prior_ir:
|
|
744
|
+
ir_channel += render_prior_ir_section(prior_ir)
|
|
745
|
+
ir_channel += render_execution_brief(prior_ir, requirements)
|
|
746
|
+
base_inputs = required_task_inputs or ""
|
|
747
|
+
channel_value = (base_inputs + ir_channel) if base_inputs else ir_channel
|
|
748
|
+
# Delivery markers are synthesized BEFORE the draft (4B class): the
|
|
749
|
+
# brief must pin them so the first emission is correctly labeled
|
|
750
|
+
# and no full re-emission is needed.
|
|
751
|
+
declared_names = sorted(
|
|
752
|
+
set(
|
|
753
|
+
re.findall(
|
|
754
|
+
r"\b[\w-]+\.(?:py|md|json|txt|cfg|toml)\b",
|
|
755
|
+
prompt_body + "\n" + plan_body + "\n" + channel_value,
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
marker_entities = tuple(f"### {n}" for n in declared_names)
|
|
760
|
+
if marker_entities:
|
|
761
|
+
channel_value += (
|
|
762
|
+
"\nDELIVERY FORMAT: file sections MUST be headed exactly by their marker lines "
|
|
763
|
+
"(e.g. a line reading exactly '### cnf.py' immediately followed by that file's code): "
|
|
764
|
+
+ "; ".join(marker_entities)
|
|
765
|
+
)
|
|
766
|
+
# DRAFT_EXECUTE (ADR-0009): entity extraction at the execute
|
|
767
|
+
# boundary, mirroring DRAFT_PROMPT's task-entity channel. Turns are
|
|
768
|
+
# structurally lossy; the draft declares the verbatim-critical
|
|
769
|
+
# entities (wire formats, constants, signatures) before any code is
|
|
770
|
+
# written, and the host enforces their survival mechanically.
|
|
771
|
+
draft_context = {
|
|
772
|
+
"HOST_PROTOCOL_STATE": "EXECUTION_DRAFT",
|
|
773
|
+
"CONFIRMED_PROMPT_BODY": prompt_body,
|
|
774
|
+
"CONFIRMED_PLAN_BODY": plan_body,
|
|
775
|
+
"REQUIRED_TASK_INPUTS": channel_value,
|
|
776
|
+
}
|
|
777
|
+
draft = self._call(
|
|
778
|
+
"DRAFT_EXECUTE",
|
|
779
|
+
draft_context,
|
|
780
|
+
traces,
|
|
781
|
+
parser=self.bridge.parse_execution_draft,
|
|
782
|
+
)
|
|
783
|
+
# Typed entity pipeline (parser over entities, not string filters):
|
|
784
|
+
# the host classifies, arithmetically validates, and applies
|
|
785
|
+
# kind-appropriate enforcement. meta entities are never enforced
|
|
786
|
+
# against the deliverable.
|
|
787
|
+
typed = parse_typed_entities(draft.execution_entities)
|
|
788
|
+
arithmetic = arithmetic_checks(typed)
|
|
789
|
+
if arithmetic:
|
|
790
|
+
self.workspace.append_event(
|
|
791
|
+
"EXECUTE_DRAFT_ARITHMETIC_RETRY", {"errors": arithmetic}
|
|
792
|
+
)
|
|
793
|
+
draft_context["REQUIRED_TASK_INPUTS"] = channel_value + (
|
|
794
|
+
"\n\n\n\nOPERATOR CORRECTION (host-side mechanical check): the declared wire-format "
|
|
795
|
+
"arithmetic is inconsistent and MUST be fixed before execution: "
|
|
796
|
+
+ " | ".join(arithmetic)
|
|
797
|
+
)
|
|
798
|
+
draft = self._call(
|
|
799
|
+
"DRAFT_EXECUTE",
|
|
800
|
+
draft_context,
|
|
801
|
+
traces,
|
|
802
|
+
parser=self.bridge.parse_execution_draft,
|
|
803
|
+
)
|
|
804
|
+
typed = parse_typed_entities(draft.execution_entities)
|
|
805
|
+
arithmetic = arithmetic_checks(typed)
|
|
806
|
+
if arithmetic:
|
|
807
|
+
self.workspace.append_event(
|
|
808
|
+
"EXECUTE_DRAFT_ARITHMETIC_INVALID",
|
|
809
|
+
{"errors": arithmetic, "scored": "model_error"},
|
|
810
|
+
)
|
|
811
|
+
containment_pool = channel_value + prompt_body + plan_body
|
|
812
|
+
kept = [e for e in typed if _norm(e["value"]) in _norm(containment_pool)]
|
|
813
|
+
dropped = len(typed) - len(kept)
|
|
814
|
+
if dropped:
|
|
815
|
+
self.workspace.append_event(
|
|
816
|
+
"EXECUTE_ENTITY_DROPPED_UNSAFE", {"count": dropped}
|
|
817
|
+
)
|
|
818
|
+
# Delivery markers are always enforced entities (4B class): the
|
|
819
|
+
# section headers themselves are lossy across turns.
|
|
820
|
+
declared_names = sorted(
|
|
821
|
+
set(
|
|
822
|
+
re.findall(
|
|
823
|
+
r"[\w-]+\.(?:py|md|json|txt|cfg|toml)",
|
|
824
|
+
prompt_body + " " + plan_body + " " + channel_value,
|
|
825
|
+
)
|
|
826
|
+
)
|
|
827
|
+
)
|
|
828
|
+
existing_values = {e["value"] for e in kept}
|
|
829
|
+
kept = kept + [
|
|
830
|
+
{"kind": "delivery_marker", "value": f"### {n}", "name": None,
|
|
831
|
+
"struct_format": None, "declared_size": None}
|
|
832
|
+
for n in declared_names
|
|
833
|
+
if f"### {n}" not in existing_values
|
|
834
|
+
]
|
|
835
|
+
brief_misses, _ = entity_enforcement_misses(kept, draft.brief_body, None)
|
|
836
|
+
if brief_misses:
|
|
837
|
+
self.workspace.append_event(
|
|
838
|
+
"EXECUTE_ENTITY_COVERAGE_RETRY", {"missing": len(brief_misses)}
|
|
839
|
+
)
|
|
840
|
+
draft_context["REQUIRED_TASK_INPUTS"] = channel_value + (
|
|
841
|
+
"\n\n\n\nOPERATOR CORRECTION (host-side mechanical check): the following execution "
|
|
842
|
+
"entities are missing from the brief body and MUST appear verbatim, "
|
|
843
|
+
"character-for-character: " + " | ".join(brief_misses)
|
|
844
|
+
)
|
|
845
|
+
draft = self._call(
|
|
846
|
+
"DRAFT_EXECUTE",
|
|
847
|
+
draft_context,
|
|
848
|
+
traces,
|
|
849
|
+
parser=self.bridge.parse_execution_draft,
|
|
850
|
+
)
|
|
851
|
+
entity_block = (
|
|
852
|
+
"\n## EXECUTION BRIEF (entity-dense; drafted prior to execution)\n"
|
|
853
|
+
+ draft.brief_body
|
|
854
|
+
+ "\nCRITICAL VERBATIM ENTITIES (each MUST appear verbatim in the deliverable; "
|
|
855
|
+
"the host checks this mechanically):\n"
|
|
856
|
+
+ "\n" + "\n".join(f"- [{e['kind']}] {e['value']}]" for e in kept)
|
|
857
|
+
+ "\nDELIVERY FORMAT: file sections MUST be headed exactly by their marker lines "
|
|
858
|
+
"(e.g. a line reading exactly '### cnf.py' immediately followed by that file's code)."
|
|
859
|
+
)
|
|
860
|
+
execute_context["REQUIRED_TASK_INPUTS"] = (
|
|
861
|
+
(base_inputs + entity_block + ir_channel) if base_inputs else (entity_block + ir_channel)
|
|
862
|
+
)
|
|
863
|
+
execute_entities = kept
|
|
864
|
+
else:
|
|
865
|
+
execute_entities = ()
|
|
866
|
+
outcome = self._call(
|
|
867
|
+
"EXECUTE",
|
|
868
|
+
execute_context,
|
|
869
|
+
traces,
|
|
870
|
+
parser=self.bridge.parse_execution,
|
|
871
|
+
)
|
|
872
|
+
final_body = outcome.body
|
|
873
|
+
if result_ir_mode and outcome.kind == "RESULT":
|
|
874
|
+
workspace_path = self.workspace.path
|
|
875
|
+
attempts = 0
|
|
876
|
+
repaired = False
|
|
877
|
+
while True:
|
|
878
|
+
wire_ir = getattr(outcome, "result_ir", None)
|
|
879
|
+
if isinstance(wire_ir, dict):
|
|
880
|
+
# Option 2 (ADR-0009): the IR arrived as a schema-enforced
|
|
881
|
+
# wire field; body scanning is only the recorded-path
|
|
882
|
+
# fallback.
|
|
883
|
+
ir = wire_ir
|
|
884
|
+
else:
|
|
885
|
+
ir = extract_result_ir(final_body)
|
|
886
|
+
if ir is not None:
|
|
887
|
+
ir_errors, _ = validate_result_ir(
|
|
888
|
+
ir, workspace_path, requirements, execution_body=final_body
|
|
889
|
+
)
|
|
890
|
+
else:
|
|
891
|
+
ir_errors = ["Result IR missing or not a JSON object (TRD-0003 RS-01)"]
|
|
892
|
+
_, entity_missing = entity_enforcement_misses(
|
|
893
|
+
execute_entities, "", final_body
|
|
894
|
+
)
|
|
895
|
+
if entity_missing:
|
|
896
|
+
self.workspace.append_event(
|
|
897
|
+
"EXECUTE_ENTITY_MISSING", {"count": len(entity_missing)}
|
|
898
|
+
)
|
|
899
|
+
errors = ir_errors + [
|
|
900
|
+
f"critical execution entity missing from the deliverable: {e!r}" for e in entity_missing
|
|
901
|
+
]
|
|
902
|
+
if not errors or attempts >= 2:
|
|
903
|
+
break
|
|
904
|
+
attempts += 1
|
|
905
|
+
if not entity_missing:
|
|
906
|
+
# IR-only repair (TRD-0003 RS-08): a dedicated
|
|
907
|
+
# schema-enforced repair op re-emits ONLY the corrected
|
|
908
|
+
# result_ir, so the retry budget affords multiple cheap
|
|
909
|
+
# attempts instead of one full re-emission.
|
|
910
|
+
repair_ctx = {
|
|
911
|
+
"HOST_PROTOCOL_STATE": "RESULT_IR_REPAIR",
|
|
912
|
+
"DELIVERABLE_BODY": final_body,
|
|
913
|
+
"RESULT_IR_REQUIREMENTS": render_instructions(
|
|
914
|
+
requirements,
|
|
915
|
+
repo_root=self.repo_root,
|
|
916
|
+
evidence_paths=evidence_paths,
|
|
917
|
+
),
|
|
918
|
+
"RESULT_IR_ERRORS": " | ".join(ir_errors),
|
|
919
|
+
}
|
|
920
|
+
repair = self._call(
|
|
921
|
+
"EMIT_RESULT_IR",
|
|
922
|
+
repair_ctx,
|
|
923
|
+
traces,
|
|
924
|
+
parser=self.bridge.parse_result_ir_repair,
|
|
925
|
+
)
|
|
926
|
+
ir2 = repair.get("result_ir") if isinstance(repair, dict) else None
|
|
927
|
+
if ir2 is not None:
|
|
928
|
+
e2, _ = validate_result_ir(
|
|
929
|
+
ir2, workspace_path, requirements, execution_body=final_body
|
|
930
|
+
)
|
|
931
|
+
if not e2:
|
|
932
|
+
fence_open = "\n\n```json\n"
|
|
933
|
+
fence_close = "\n```"
|
|
934
|
+
final_body = (
|
|
935
|
+
final_body
|
|
936
|
+
+ "\n\n"
|
|
937
|
+
+ fence_open
|
|
938
|
+
+ json.dumps(ir2, indent=2, ensure_ascii=False)
|
|
939
|
+
+ fence_close
|
|
940
|
+
)
|
|
941
|
+
repaired = True
|
|
942
|
+
self.workspace.append_event(
|
|
943
|
+
"RESULT_IR_REPAIRED", {"attempts": attempts}
|
|
944
|
+
)
|
|
945
|
+
break
|
|
946
|
+
ir_errors = e2
|
|
947
|
+
self.workspace.append_event(
|
|
948
|
+
"RESULT_IR_REPAIR_RETRY", {"attempt": attempts}
|
|
949
|
+
)
|
|
950
|
+
continue
|
|
951
|
+
# Code-defect retry: the deliverable itself is wrong; a full
|
|
952
|
+
# re-emission is unavoidable (option 2: body + IR travel
|
|
953
|
+
# together under the wire schema).
|
|
954
|
+
correction_ctx = dict(execute_context)
|
|
955
|
+
correction_ctx["REQUIRED_TASK_INPUTS"] = execute_context["REQUIRED_TASK_INPUTS"] + (
|
|
956
|
+
"\\n\\nRESULT IR VALIDATION ERRORS (host-side mechanical check): fix ONLY these violations and re-emit the FULL response with a corrected result_ir: "
|
|
957
|
+
+ " | ".join(errors)
|
|
958
|
+
)
|
|
959
|
+
outcome = self._call(
|
|
960
|
+
"EXECUTE",
|
|
961
|
+
correction_ctx,
|
|
962
|
+
traces,
|
|
963
|
+
parser=self.bridge.parse_execution,
|
|
964
|
+
)
|
|
965
|
+
final_body = outcome.body
|
|
966
|
+
if repaired:
|
|
967
|
+
self.workspace.append_event(
|
|
968
|
+
"RESULT_IR_VALIDATED", {"ir": ir, "repaired": True}
|
|
969
|
+
)
|
|
970
|
+
elif errors:
|
|
971
|
+
self.workspace.append_event(
|
|
972
|
+
"RESULT_IR_INVALID", {"errors": errors, "scored": "model_error"}
|
|
973
|
+
)
|
|
974
|
+
else:
|
|
975
|
+
self.workspace.append_event("RESULT_IR_VALIDATED", {"ir": ir})
|
|
976
|
+
if outcome.kind == "REQUEST_INPUT":
|
|
977
|
+
assert outcome.expected_type is not None and outcome.description is not None
|
|
978
|
+
self.controller.request_execution_input(outcome.expected_type, outcome.description)
|
|
979
|
+
self.workspace.publish_execution_outcome(
|
|
980
|
+
outcome.kind,
|
|
981
|
+
outcome.body,
|
|
982
|
+
{"expected_type": outcome.expected_type, "description": outcome.description},
|
|
983
|
+
)
|
|
984
|
+
return EngineResponse(outcome.body, traces)
|
|
985
|
+
if outcome.kind == "BLOCKED_BY_HIGHER_PRIORITY":
|
|
986
|
+
self.controller.cancel()
|
|
987
|
+
self.workspace.publish_execution_outcome(outcome.kind, outcome.body)
|
|
988
|
+
return EngineResponse(outcome.body, traces, closed=True)
|
|
989
|
+
result_body_hash = hashlib.sha256(final_body.encode("utf-8")).hexdigest()
|
|
990
|
+
self.controller.complete_success(result_body_hash)
|
|
991
|
+
if self.workspace.turn_id is not None:
|
|
992
|
+
self.workspace.mark_turn_status("CLOSED_SUCCESS", deliverable_sha256=result_body_hash)
|
|
993
|
+
self.workspace.publish_execution_outcome(
|
|
994
|
+
outcome.kind,
|
|
995
|
+
final_body,
|
|
996
|
+
{
|
|
997
|
+
"source_prompt_id": prompt.artifact_id,
|
|
998
|
+
"source_plan_id": plan.artifact_id,
|
|
999
|
+
"result_body_hash": result_body_hash,
|
|
1000
|
+
"confirmed_prompt_hash": hashlib.sha256(prompt.body.encode("utf-8")).hexdigest(),
|
|
1001
|
+
"confirmed_plan_hash": hashlib.sha256(plan.body.encode("utf-8")).hexdigest(),
|
|
1002
|
+
},
|
|
1003
|
+
)
|
|
1004
|
+
return EngineResponse(final_body, traces, closed=True)
|
|
1005
|
+
|
|
1006
|
+
def _answer_protocol(self, user_message: str, traces: list[CallTrace]) -> EngineResponse:
|
|
1007
|
+
assert self.controller is not None
|
|
1008
|
+
kind, body = self.controller.review_subject()
|
|
1009
|
+
return EngineResponse(self._call(
|
|
1010
|
+
"ANSWER_PROTOCOL_DISCUSSION",
|
|
1011
|
+
{
|
|
1012
|
+
"RAW_PROTOCOL_QUESTION": user_message,
|
|
1013
|
+
"CURRENT_STAGE_CLASS": self.controller.state.stage.value,
|
|
1014
|
+
"BOUND_REVIEW_SUBJECT_KIND": kind,
|
|
1015
|
+
"BOUND_REVIEW_SUBJECT_BODY": body,
|
|
1016
|
+
},
|
|
1017
|
+
traces,
|
|
1018
|
+
parser=self.bridge.parse_protocol_discussion,
|
|
1019
|
+
), traces)
|
|
1020
|
+
|
|
1021
|
+
def _apply_transition(self, transition: Transition, user_message: str, traces: list[CallTrace]) -> EngineResponse:
|
|
1022
|
+
if transition.action == NextAction.DRAFT_PLAN:
|
|
1023
|
+
return self._draft_plan(transition, traces)
|
|
1024
|
+
if transition.action == NextAction.REVISE_PROMPT:
|
|
1025
|
+
return self._revise_prompt(transition, traces)
|
|
1026
|
+
if transition.action == NextAction.REVISE_PLAN:
|
|
1027
|
+
return self._revise_plan(transition, traces)
|
|
1028
|
+
if transition.action == NextAction.EXECUTE:
|
|
1029
|
+
return self._execute(transition, traces)
|
|
1030
|
+
if transition.action == NextAction.ANSWER_PROTOCOL:
|
|
1031
|
+
return self._answer_protocol(user_message, traces)
|
|
1032
|
+
if transition.action == NextAction.DEFER_SUBSTANTIVE:
|
|
1033
|
+
return EngineResponse(presentation.deferred_substantive(), traces)
|
|
1034
|
+
if transition.action == NextAction.REQUEST_REVIEW_CLARIFICATION:
|
|
1035
|
+
return EngineResponse(presentation.review_clarification(), traces)
|
|
1036
|
+
if transition.action == NextAction.SHOW_CURRENT_PROMPT:
|
|
1037
|
+
assert self.controller is not None and self.workspace is not None
|
|
1038
|
+
prompt = self.controller.state.current_prompt
|
|
1039
|
+
assert prompt is not None
|
|
1040
|
+
self.workspace.publish_approach_sources(list(self.controller.state.approach_sources))
|
|
1041
|
+
return EngineResponse(presentation.prompt_artifact(self.workspace.read_artifact("prompt")[1]), traces)
|
|
1042
|
+
if transition.action == NextAction.CLOSED:
|
|
1043
|
+
assert self.workspace is not None
|
|
1044
|
+
self.workspace.append_event("PROTOCOL_CLOSED", {"reason": "cancelled"})
|
|
1045
|
+
return EngineResponse(presentation.cancelled(), traces, closed=True)
|
|
1046
|
+
if transition.action == NextAction.START_NEW_INSTANCE:
|
|
1047
|
+
new_task = transition.payload["new_task_source"]
|
|
1048
|
+
assert self.workspace is not None
|
|
1049
|
+
self.workspace.append_event("PROTOCOL_CLOSED", {"reason": "new_task"})
|
|
1050
|
+
self.workspace = self._new_workspace()
|
|
1051
|
+
self.controller = None
|
|
1052
|
+
return self._draft_initial_prompt(
|
|
1053
|
+
new_task,
|
|
1054
|
+
traces,
|
|
1055
|
+
protocol_state="ACTIVE_FRESH_INSTANCE_FROM_REVIEW",
|
|
1056
|
+
)
|
|
1057
|
+
raise ControllerError(f"transition:{transition.action.value}")
|
|
1058
|
+
|
|
1059
|
+
def handle_explicit_review(self, intent: Intent, feedback: str | None = None) -> EngineResponse:
|
|
1060
|
+
"""Directly apply a review intent without LLM interpretation overhead (fast-path)."""
|
|
1061
|
+
traces: list[CallTrace] = []
|
|
1062
|
+
if self.workspace is None:
|
|
1063
|
+
raise WorkspaceError("active_controller_without_workspace")
|
|
1064
|
+
if self.controller is None:
|
|
1065
|
+
raise ControllerError("user_message_stage")
|
|
1066
|
+
# Durability (ADR-0009 workflow): interrupted internal transitions are
|
|
1067
|
+
# re-driven by explicit user input instead of bricking the epoch.
|
|
1068
|
+
if self.controller.state.stage == Stage.PLAN_REQUIRED:
|
|
1069
|
+
return self._draft_plan(Transition(NextAction.DRAFT_PLAN, {}), traces)
|
|
1070
|
+
if self.controller.state.stage == Stage.EXECUTION_READY:
|
|
1071
|
+
return self._execute(Transition(NextAction.EXECUTE, {}), traces)
|
|
1072
|
+
if self.controller.state.stage not in {Stage.PROMPT_REVIEW, Stage.PLAN_REVIEW, Stage.WAITING_INPUT}:
|
|
1073
|
+
raise ControllerError("user_message_stage")
|
|
1074
|
+
|
|
1075
|
+
self._sync_review_edit()
|
|
1076
|
+
previous_stage = self.controller.state.stage
|
|
1077
|
+
decision = ReviewDecision(intent=intent)
|
|
1078
|
+
msg = feedback or ""
|
|
1079
|
+
transition = self.controller.apply_review_decision(decision, msg)
|
|
1080
|
+
if decision.intent == Intent.ACCEPT_CURRENT:
|
|
1081
|
+
if previous_stage == Stage.PROMPT_REVIEW:
|
|
1082
|
+
prompt = self.controller.state.current_prompt
|
|
1083
|
+
assert prompt is not None
|
|
1084
|
+
self.workspace.mark_artifact_confirmed("prompt", prompt.artifact_id)
|
|
1085
|
+
elif previous_stage == Stage.PLAN_REVIEW:
|
|
1086
|
+
plan = self.controller.state.current_plan
|
|
1087
|
+
assert plan is not None
|
|
1088
|
+
self.workspace.mark_artifact_confirmed("plan", plan.artifact_id)
|
|
1089
|
+
if decision.intent == Intent.REVISE_APPROACH and previous_stage == Stage.PROMPT_REVIEW:
|
|
1090
|
+
self.workspace.publish_approach_sources(list(self.controller.state.approach_sources))
|
|
1091
|
+
return self._apply_transition(transition, msg, traces)
|
|
1092
|
+
|
|
1093
|
+
def handle_user_message(self, user_message: str) -> EngineResponse:
|
|
1094
|
+
traces: list[CallTrace] = []
|
|
1095
|
+
if self.controller is None or self.controller.state.stage in {Stage.CLOSED_SUCCESS, Stage.CLOSED_CANCELLED}:
|
|
1096
|
+
return self._activation(user_message, traces) or EngineResponse(None, traces, bypass=True)
|
|
1097
|
+
|
|
1098
|
+
if self.workspace is None:
|
|
1099
|
+
raise WorkspaceError("active_controller_without_workspace")
|
|
1100
|
+
# Durability (ADR-0009 workflow): interrupted internal transitions are
|
|
1101
|
+
# re-driven by any user input instead of bricking the epoch.
|
|
1102
|
+
if self.controller.state.stage == Stage.PLAN_REQUIRED:
|
|
1103
|
+
return self._draft_plan(Transition(NextAction.DRAFT_PLAN, {}), traces)
|
|
1104
|
+
if self.controller.state.stage == Stage.EXECUTION_READY:
|
|
1105
|
+
return self._execute(Transition(NextAction.EXECUTE, {}), traces)
|
|
1106
|
+
if self.controller.state.stage not in {Stage.PROMPT_REVIEW, Stage.PLAN_REVIEW, Stage.WAITING_INPUT}:
|
|
1107
|
+
raise ControllerError("user_message_stage")
|
|
1108
|
+
|
|
1109
|
+
stripped = user_message.strip()
|
|
1110
|
+
# Silence-deferral fix (Finding D3): do not route empty/whitespace input to LLM interpretation
|
|
1111
|
+
if not stripped:
|
|
1112
|
+
artifact_name = "prompt pseudocode" if self.controller.state.stage == Stage.PROMPT_REVIEW else "execution plan"
|
|
1113
|
+
return EngineResponse(
|
|
1114
|
+
f"Please confirm the {artifact_name} (type /confirm or press Enter with confirmation) or specify revisions (/revise <feedback>).",
|
|
1115
|
+
traces,
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
# Fast-path commands
|
|
1119
|
+
lower = stripped.lower()
|
|
1120
|
+
if lower == "/confirm":
|
|
1121
|
+
return self.handle_explicit_review(Intent.ACCEPT_CURRENT)
|
|
1122
|
+
if lower.startswith("/revise"):
|
|
1123
|
+
fb = stripped[7:].strip()
|
|
1124
|
+
if not fb:
|
|
1125
|
+
return EngineResponse("Please specify your revisions: /revise <feedback>", traces)
|
|
1126
|
+
target_intent = (
|
|
1127
|
+
Intent.REVISE_TASK
|
|
1128
|
+
if self.controller.state.stage == Stage.PROMPT_REVIEW
|
|
1129
|
+
else Intent.REVISE_APPROACH
|
|
1130
|
+
)
|
|
1131
|
+
return self.handle_explicit_review(target_intent, fb)
|
|
1132
|
+
if lower == "/stop":
|
|
1133
|
+
return self.handle_explicit_review(Intent.CANCEL)
|
|
1134
|
+
|
|
1135
|
+
# Standard LLM review interpretation
|
|
1136
|
+
self._sync_review_edit()
|
|
1137
|
+
subject_kind, subject_body = self.controller.review_subject()
|
|
1138
|
+
previous_stage = self.controller.state.stage
|
|
1139
|
+
operation, parser = {
|
|
1140
|
+
Stage.PROMPT_REVIEW: ("INTERPRET_PROMPT_REVIEW", self.bridge.parse_prompt_review),
|
|
1141
|
+
Stage.PLAN_REVIEW: ("INTERPRET_PLAN_REVIEW", self.bridge.parse_plan_review),
|
|
1142
|
+
Stage.WAITING_INPUT: ("INTERPRET_EXECUTION_INPUT", self.bridge.parse_execution_input),
|
|
1143
|
+
}[previous_stage]
|
|
1144
|
+
decision = ReviewDecision.from_dict(self._call(
|
|
1145
|
+
operation,
|
|
1146
|
+
{
|
|
1147
|
+
"BOUND_REVIEW_SUBJECT_KIND": subject_kind,
|
|
1148
|
+
"BOUND_REVIEW_SUBJECT_BODY": subject_body,
|
|
1149
|
+
"RAW_USER_REVIEW_MESSAGE": user_message,
|
|
1150
|
+
},
|
|
1151
|
+
traces,
|
|
1152
|
+
parser=parser,
|
|
1153
|
+
))
|
|
1154
|
+
transition = self.controller.apply_review_decision(decision, user_message)
|
|
1155
|
+
|
|
1156
|
+
if decision.intent == Intent.ACCEPT_CURRENT:
|
|
1157
|
+
if previous_stage == Stage.PROMPT_REVIEW:
|
|
1158
|
+
prompt = self.controller.state.current_prompt
|
|
1159
|
+
assert prompt is not None
|
|
1160
|
+
self.workspace.mark_artifact_confirmed("prompt", prompt.artifact_id)
|
|
1161
|
+
elif previous_stage == Stage.PLAN_REVIEW:
|
|
1162
|
+
plan = self.controller.state.current_plan
|
|
1163
|
+
assert plan is not None
|
|
1164
|
+
self.workspace.mark_artifact_confirmed("plan", plan.artifact_id)
|
|
1165
|
+
if decision.intent == Intent.REVISE_APPROACH and previous_stage == Stage.PROMPT_REVIEW:
|
|
1166
|
+
self.workspace.publish_approach_sources(list(self.controller.state.approach_sources))
|
|
1167
|
+
|
|
1168
|
+
return self._apply_transition(transition, user_message, traces)
|