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
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail-closed marker for a dead research lane of the deep-research graph.
|
||||
|
||||
Reached ONLY via research_one_question's `fallback:` route inside the
|
||||
`research_each_question` map branch (branch-local — a map branch's fallback
|
||||
must stay inside the branch subgraph). Grounded in engine behavior: on
|
||||
llm-node failure the engine writes "LLM node …failed: <chain>" into
|
||||
`finding` via the node's state_updates BEFORE routing to the fallback (on
|
||||
success the same key holds the researched findings, which never start with
|
||||
the "LLM node" prefix). This node normalizes the failure text into a
|
||||
PIPELINE-FAULT finding and the chain ends here — the map collects the fault
|
||||
as the lane's `finding`, so one dead research lane never sinks the whole
|
||||
map. combine_findings later lifts PIPELINE-FAULT findings into
|
||||
`pipeline_faults` so the fault also surfaces in the report's Pipeline notes.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
MAX_DETAIL_CHARS = 500
|
||||
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def fault_finding(detail):
|
||||
return {
|
||||
"finding": (
|
||||
f"PIPELINE-FAULT: question research failed — finding unavailable — {detail}"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
raw = state.get("finding")
|
||||
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||
if not detail.startswith("LLM node"):
|
||||
detail = "the research lane died without recording a failure detail"
|
||||
if len(detail) > MAX_DETAIL_CHARS:
|
||||
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||
print(json.dumps(fault_finding(detail)))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||
print(json.dumps(fault_finding(f"fault-marker script error: {e}")))
|
||||
Reference in new issue
Block a user