feat(TASK-023): deep-research hardening — fail-safe scripts, fault fallbacks, pipeline notes
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.
This commit is contained in:
13 files changed
+1111
-10
No files matched your search
@@ -6,6 +6,18 @@ one entry per sub-question, in input order). Downstream nodes
|
||||
(`vet_sources`, `critique`, `synthesize`) read `{{findings}}` as a
|
||||
single block, so this script renders the array as a Markdown document
|
||||
with one section per question.
|
||||
|
||||
Fault visibility: a research lane that died lands here as a finding
|
||||
starting with "PIPELINE-FAULT:" (written by question_fault inside the map
|
||||
branch, where `pipeline_faults` itself is out of reach — map branches only
|
||||
surface their collected output_key). This script lifts such findings into
|
||||
`pipeline_faults` (deduplicated, since a reflexion or feedback loop can
|
||||
re-run the map) so dead lanes surface in the report's Pipeline notes.
|
||||
|
||||
Fail-safe (R3): on a crash (e.g. malformed GRAPH_STATE), `findings`
|
||||
becomes the fault text itself and a PIPELINE-FAULT entry is recorded —
|
||||
the pipeline proceeds visibly degraded, and the human approval gate is
|
||||
the backstop. Existing faults are preserved best-effort.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -19,21 +31,40 @@ def load_state():
|
||||
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 main():
|
||||
state = load_state()
|
||||
questions = state.get("questions") or []
|
||||
per_question = state.get("question_findings") or []
|
||||
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||
|
||||
sections = []
|
||||
for idx, q in enumerate(questions):
|
||||
body = per_question[idx] if idx < len(per_question) else ""
|
||||
if isinstance(body, dict) or isinstance(body, list):
|
||||
body = json.dumps(body, indent=2)
|
||||
if isinstance(body, str) and body.startswith("PIPELINE-FAULT:") and body not in faults:
|
||||
faults.append(body)
|
||||
sections.append(f"## {q}\n\n{body}")
|
||||
|
||||
findings = "\n\n".join(sections) if sections else "No findings gathered."
|
||||
print(json.dumps({"findings": findings}))
|
||||
print(json.dumps({"findings": findings, "pipeline_faults": faults}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001 — a crashed join must degrade, not die
|
||||
faults = safe_faults()
|
||||
fault = (
|
||||
f"PIPELINE-FAULT: combining findings crashed — {e}; per-question "
|
||||
"findings may exist in state but could not be joined"
|
||||
)
|
||||
faults.append(fault)
|
||||
print(json.dumps({"findings": fault, "pipeline_faults": faults}))
|
||||
Reference in new issue
Block a user