R3 guards: parse_request (crash → ask_topic), bootstrap_research,
combine_findings (crash → fault-text findings; also lifts branch-local
PIPELINE-FAULT findings into pipeline_faults, deduped), reflexion_gate
(crash fails forward to synthesize), incorporate_feedback (crash still
loops with a generic feedback note), verify_sources (top-level guard +
pipeline_notes fold).
Fallbacks per the plan's naming pins: plan → plan_fault → end_fault
(fail-closed; gains state_updates plan_failure per the TASK-016 rule);
research_one_question → branch-local question_fault (map collects the
fault finding); vet_sources/critique → degrade-visibly markers that
continue to the stage each node's own next pointed to; synthesize →
synth_fault → end_fault. end_fault renders
'DEEP_RESEARCH FAILED — PIPELINE-FAULT: <fault text>'.
Accepted-path visibility: verify_sources folds pipeline_faults into
pipeline_notes ('' when fault-free), appended to end_accepted's output
and the approval question — happy-path output stays byte-identical.
initial_state seeds questions: [] so a dying plan inside the static
fan-out leaves the sibling knowledge_lookup lane mapping over zero
items while end_fault renders.
R5: 19 python3-guarded regression tests + GraphParser wiring test;
bundled warning baseline unchanged.
98 lines
3.1 KiB
Python
98 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Reflexion gate for deep-research.
|
|
|
|
Runs after `critique` has reviewed the current research findings. If the
|
|
critique's verdict is REVISE and the reflexion budget is not spent,
|
|
loops back to `research` with the critique attached as
|
|
`research_feedback`, so the retry is informed rather than a blind
|
|
re-run. Otherwise it proceeds to `synthesize`.
|
|
|
|
Routing (`_next`):
|
|
- verdict PASS -> synthesize
|
|
- verdict REVISE, budget remaining -> research_each_question (+ research_feedback)
|
|
- verdict REVISE, budget spent -> synthesize
|
|
|
|
Reflexion is a best-effort quality booster, not a hard gate: once the
|
|
budget is spent the workflow proceeds anyway, and the human approval
|
|
step is the final backstop.
|
|
|
|
Fail-safe (R3): a crash here fails FORWARD to `synthesize` — the same
|
|
direction as the existing malformed-critique PASS default — with a
|
|
PIPELINE-FAULT entry recorded (best-effort preserving existing faults),
|
|
so a broken gate costs the run its automated critique routing, never the
|
|
run itself.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
# Automated revision passes allowed. `research` runs at most
|
|
# MAX_REFLEXION_REVISIONS + 1 times per user pass. Bump to allow more.
|
|
MAX_REFLEXION_REVISIONS = 2
|
|
|
|
|
|
def load_state():
|
|
path = os.environ.get("GRAPH_STATE_FILE")
|
|
if path:
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
|
|
|
|
|
def safe_faults():
|
|
try:
|
|
state = load_state()
|
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
|
return []
|
|
|
|
|
|
def as_int(value, default=0):
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def parse_verdict(critique):
|
|
"""Pull PASS/REVISE from the critique's `VERDICT:` line. Defaults to
|
|
PASS when no verdict line is found, so a malformed critique lets the
|
|
workflow proceed instead of burning the whole revision budget."""
|
|
match = re.search(r"VERDICT:\s*([A-Za-z]+)", critique, re.IGNORECASE)
|
|
if not match:
|
|
return "PASS"
|
|
return match.group(1).upper()
|
|
|
|
|
|
def main():
|
|
state = load_state()
|
|
critique = state.get("critique") or ""
|
|
verdict = parse_verdict(critique)
|
|
attempts = as_int(state.get("research_attempts"))
|
|
|
|
if verdict == "REVISE" and attempts < MAX_REFLEXION_REVISIONS:
|
|
feedback = (
|
|
"A reviewer judged the previous research pass incomplete. "
|
|
"Address every point in the critique below:\n\n" + critique
|
|
)
|
|
output = {
|
|
"_next": "research_each_question",
|
|
"research_attempts": attempts + 1,
|
|
"research_feedback": feedback,
|
|
}
|
|
else:
|
|
output = {"_next": "synthesize"}
|
|
|
|
print(json.dumps(output))
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as e: # noqa: BLE001 — a crashed gate fails forward, never dies
|
|
faults = safe_faults()
|
|
faults.append(
|
|
f"PIPELINE-FAULT: reflexion gate crashed — {e}; proceeded to synthesis "
|
|
"without automated critique routing"
|
|
)
|
|
print(json.dumps({"_next": "synthesize", "pipeline_faults": faults}))
|