feat: Deep research upgrade so sisyphus and the architect now have 3 possible research types: librarian standard, librarian deep, and true deep research with reflexion
This commit is contained in:
9 files changed
+171
-62
No files matched your search
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Autonomy router for deep-research's approval gate.
|
||||
|
||||
Runs after verify_sources. When the `autonomy` variable is
|
||||
'autonomous' (the run was spawned by another agent — architect or
|
||||
sisyphus research tiers — with no human at the terminal), the report
|
||||
skips the human approval node and ends accepted: the reflexion loop
|
||||
already served as the quality gate, and end_autonomous carries the
|
||||
source check + pipeline notes with the report so the caller sees them.
|
||||
Any other value (default 'supervised') proceeds to the human approval
|
||||
gate unchanged.
|
||||
|
||||
Routing (`_next`):
|
||||
- autonomy == 'autonomous' -> end_autonomous
|
||||
- anything else -> approve
|
||||
|
||||
Fail-safe: a crash here fails FORWARD via the node's `next` to
|
||||
`approve` — the safe, human-gated direction.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
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 main():
|
||||
state = load_state()
|
||||
autonomy = str(state.get("autonomy") or "supervised").strip().lower()
|
||||
target = "end_autonomous" if autonomy == "autonomous" else "approve"
|
||||
print(json.dumps({"_next": target}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -4,10 +4,11 @@
|
||||
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
|
||||
branch failure the engine writes its failure text into `finding` via the
|
||||
node's state_updates BEFORE routing to the fallback — "Agent node failed:
|
||||
<chain>" now that the branch spawns a librarian agent, "LLM node …failed:
|
||||
<chain>" for an llm branch (on success the same key holds the researched
|
||||
findings, which never start with either 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
|
||||
@@ -53,7 +54,7 @@ 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"):
|
||||
if not detail.startswith(("LLM node", "Agent node")):
|
||||
detail = "the research lane died without recording a failure detail"
|
||||
if len(detail) > MAX_DETAIL_CHARS:
|
||||
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"""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,
|
||||
critique's verdict is REVISE and the reflexion budget (the graph's
|
||||
`reflexion_budget` variable, default 2) 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`.
|
||||
@@ -26,9 +27,11 @@ 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
|
||||
# Default automated revision passes when the `reflexion_budget` variable
|
||||
# is absent or malformed. `research` runs at most budget + 1 times per
|
||||
# user pass. Callers raise it via --variables {"reflexion_budget": "6"}
|
||||
# to let the run churn until the critique passes.
|
||||
DEFAULT_REFLEXION_REVISIONS = 2
|
||||
|
||||
|
||||
def load_state():
|
||||
@@ -69,8 +72,9 @@ def main():
|
||||
critique = state.get("critique") or ""
|
||||
verdict = parse_verdict(critique)
|
||||
attempts = as_int(state.get("research_attempts"))
|
||||
budget = as_int(state.get("reflexion_budget"), DEFAULT_REFLEXION_REVISIONS)
|
||||
|
||||
if verdict == "REVISE" and attempts < MAX_REFLEXION_REVISIONS:
|
||||
if verdict == "REVISE" and attempts < budget:
|
||||
feedback = (
|
||||
"A reviewer judged the previous research pass incomplete. "
|
||||
"Address every point in the critique below:\n\n" + critique
|
||||
|
||||
Reference in new issue
Block a user