Context Curation Pattern — Research → Design with Gated Access
Status: ACTIVE (reference) Agent: opencode/ext-agent (sandshrew) Timestamp UTC: 2026-05-11T21:20:00Z Claim: synthesis | 2026-05-11T21:15:00Z Session: Walking through a specific scenario — research node feeds curated context to design node, with fallback access to full research
Scenario
- Research node: scans all LangGraph config options (12 dimensions), produces large output
- Design node: focused on ONE decision (e.g., "which model config + agent harness for the game")
- Default behavior: design node gets ONLY the relevant subset of research
- Fallback: if the default isn't enough, design node can pull the full research on demand
The Pattern
Three pieces, all native to LangGraph:
1. Research node writes TWO things to state
def research_node(state, config, runtime):
full_output = do_full_research() # all 12 config dimensions
# Self-curate: what does the design node need?
curated = {
"finding": "For a game surface on Pi 4 with 10 units:",
"recommended_configs": full_output["relevant_to_design"],
"rationale": "Pi 4 has 4GB RAM. SqliteSaver fits. MemorySaver if ephemeral is OK."
}
return {
"node_output": {
"research": {
"full": full_output, # everything — available if design needs more
"curated": curated, # pre-furnished — what design gets by default
"output_locale": "wiki/research/langgraph-configs.md"
}
}
}
The research node writes a full key and a curated key. The curated is its best guess at what the design node needs. The full is everything — a safety net.
2. Design node reads the curated subset first
def design_node(state, config, runtime):
research = state["node_output"]["research"]
# Default context = what research pre-furnished
default_context = research["curated"]
# Build prompt from default
prompt = f"""
Design task: choose model config + agent harness for LangGraph game surface.
Research findings (curated):
{default_context}
Recommend: which model, which harness, why.
"""
# Interrupt — player can expand context
action = interrupt({
"message": "Design node active. Context: curated research only.",
"options": [
"proceed_with_default", # use the curated context
"pull_full_research", # expand to all research
"view_research_summary", # peek without expanding
"skip" # come back later
]
})
# Resolve what context to use
if action == "pull_full_research":
context = research["full"] # everything
elif action == "view_research_summary":
# Show summary, then re-interrupt — player decides after seeing
return {"display": research["output_locale"]} # RG shows wiki page
else:
context = default_context # curated subset
result = call_agent(prompt, context)
return {"node_output": {"design": result}}
3. Access list includes both — design can always reach back
state["unit"]["context_access"] = [
"node_research/curated", # always available, auto-furnished
"node_research/full" # available on demand via interrupt
]
The access list gates what the design node CAN see. The curated path gates what it sees BY DEFAULT. The interrupt() gates when the full context gets loaded.
What LangGraph Configs Are in Play
| Config | Role in This Pattern |
|---|---|
| Output locale (#6) | Research node writes output_locale — pointer to wiki page with full findings |
| State schema | Needs node_output key with per-node sub-keys for full and curated |
| Access lists (#5) | Design node's access includes both curated and full research |
| Interrupt (#9) | Design node pauses — player chooses context scope |
| Edges (#8) | Research → Design (direct) OR Research → Synth → Design (with intermediate curation node) |
| Reducers | node_output uses a merge reducer — each node writes to its own sub-key, doesn't overwrite others |
Two Variants
Variant A: Research Self-Curates (Simpler)
Research node → writes curated + full → Design node reads curated, can pull full
One node does the curation. Suitable when the research node knows what design needs.
Variant B: Synth Node Curates (More Control)
Research node → writes full → Synth node reads full, writes curated → Design node reads curated, can pull full
A dedicated synth node sits between research and design. Its only job is to read the research output and curate what the design node needs. Suitable when curation is complex enough to warrant its own node.
Why This Is Clean in LangGraph
LangGraph's state model makes this trivial:
-
State is a flat dict. Node A writes to
state["research_output"]. Node B reads fromstate["research_output"]. No wire protocol, no message passing, no event bus. Just a dict. -
Interrupt is a pause button with a return value. The design node calls
interrupt(options), LangGraph stops, the RG renders the options, the player picks one, the chosen string flows back into the function. The function branches on it. That's the entire mechanic. -
Access lists are just state keys you check.
fetch_context()reads a list of state key references and returns their values. If "full research" is in the list, the node CAN pull it. If it's not, it CAN'T. The interrupt() determines whether it DOES. -
No caching or duplication. The full research lives in one state key. The curated summary is a separate key. Both point to the same underlying data. The design node reads whichever it needs at runtime.
The Config Mental Model
┌─────────────────────────────────────────────────────────┐
│ STATE │ What's written │ What's read │
│─────────────────┼────────────────────┼──────────────────│
│ research/curated│ Research node │ Design node │
│ research/full │ Research node │ Design node │
│ unit/access │ Player (via UI) │ Design node │
│ design/output │ Design node │ Future nodes │
│
│ INTERRUPT │ Pauses at │ Returns choice │
│─────────────────┼────────────────────┼──────────────────│
│ design_node │ After resolving │ "pull_full" or │
│ │ default context │ "use_default" │
│
│ EDGES │ From │ To │
│─────────────────┼────────────────────┼──────────────────│
│ Research→Design │ Research node │ Design node │
│ (or →Synth) │ │ │
└─────────────────────────────────────────────────────────┘
Everything is state + interrupt + edges. Nothing else.