Reasoning¶
A model that loops without thinking just pays you to be wrong faster. Tulip ships three reasoning add-ons. Each catches a different class of mistake before the next tool call:
- Reflexion catches wrong premises — the agent self-evaluates after each turn and re-plans if the last step was a dead end, instead of stacking another tool call on top.
- Grounding catches hallucinations — before the answer ships, every factual claim is checked against the tool results that produced it; unsupported claims are dropped or sent back.
- Causal reasoning catches contradictions — a cause-effect graph surfaces the case where turn 3's "fix" contradicts turn 1's "root cause", which linear chat history hides.
Reflexion and grounding are each a single argument on Agent(...) and
combine freely. Causal reasoning is a standalone graph builder
(build_causal_chain()) you run over the events the agent surfaced —
see its section below.
When to pick which¶
| Situation | Add-on |
|---|---|
| Agent loops endlessly or stacks tool calls on a wrong premise | reflexion=True |
| Analyst-facing findings where a hallucinated fact drives a wrong action (indicator values, hostnames, CVE ids) | grounding=True |
| Multi-step investigation or root-cause analysis where one bad assumption poisons the chain | build_causal_chain(events) |
| All three apply — production triage agent, audit-sensitive finding | reflexion + grounding on the agent, causal chain over its events |
| Quick prototype, low-stakes lookup | leave them off — extra model calls are wasted |
The cost is more model round-trips. The win is fewer wrong answers. For short tasks the math doesn't pencil out. For runs of 5+ tool calls or anything that ships to an analyst, it almost always does.
Getting started¶
Reflexion¶
Self-evaluate per turn.
from tulip.agent import Agent
agent = Agent(
model="anthropic:claude-sonnet-4-6",
tools=[query_siem, summarise],
reflexion=True,
)
result = agent.run_sync("Find the lateral-movement events for INC-92 and explain the spread.")
print(result.metrics.reflexion_evaluations)
After each tool result, the agent is asked: given this, was the last
step right? If the answer is "no", the next turn rewrites the plan
instead of stacking another tool call on top. Streamed as
ReflectEvent — render it in your UI and the user can literally watch
the agent change its mind.
Grounding¶
Verify claims before answering.
agent = Agent(
model="anthropic:claude-sonnet-4-6",
tools=[enrich_indicator, lookup_hash],
grounding=True,
)
result = agent.run_sync("Is the hash 44d88612... associated with known malware?")
print(f"grounding score: {result.grounding_score}")
for claim in result.ungrounded_claims:
print(f"DROPPED: {claim}")
Before the agent finalises an answer, every factual claim is checked
against the conversation's tool results. A second model — the judge —
reads each claim and the supporting tool output and emits supported /
unsupported / partially supported. Unsupported claims are dropped or
sent back for re-research. Streamed as GroundingEvent.
Causal¶
Track cause-effect chains. Unlike reflexion and grounding, causal
reasoning isn't an Agent(...) flag — it's a standalone builder,
build_causal_chain(), that you run over the events your agent or
tools surfaced. It returns a CausalChain you can summarise or query.
from tulip.reasoning.causal import build_causal_chain
# Events the agent extracted from the investigation, each optionally
# naming its cause(s) by label.
chain = build_causal_chain([
{"label": "Phished credential for svc-backup"},
{"label": "SMB session to DC01",
"causes": ["Phished credential for svc-backup"]},
{"label": "Domain admin token minted",
"causes": ["SMB session to DC01"]},
])
summary = chain.get_chain_summary()
print(summary["root_causes"]) # ['Phished credential for svc-backup']
print(summary["symptoms"]) # ['Domain admin token minted']
print(summary["conflicts"]) # count of contradictory edges detected
The chain is a cause-effect graph — X happened because Y; Y because
Z — that auto-classifies each node as root cause, intermediate, or
symptom, and surfaces cycles and contradictions as the graph grows.
Particularly useful for incident triage where the linear chat log
doesn't show that turn 3's "fix" contradicts turn 1's "root cause".
build_causal_chain() is plain Python with no model call — feed it
the events and read the structure back.
Combining them¶
Turn on the two in-loop add-ons together, then build the causal graph over the events the run surfaced:
agent = Agent(
model="anthropic:claude-sonnet-4-6",
tools=[...],
reflexion=True,
grounding=True,
)
result = agent.run_sync("How did the attacker reach the domain controller?")
# Build the cause-effect graph from the events the agent extracted.
from tulip.reasoning.causal import build_causal_chain
chain = build_causal_chain(events_from(result)) # your event extractor
The order is fixed for the in-loop add-ons: reflect first, ground last.
Reflect first because if the last step was wrong, grounding on its
claims is wasted judge work; ground last because intermediate claims
will be rewritten before they ship, so any tokens spent verifying them
are tokens spent verifying drafts. Reflexion and grounding are
observable as their own event types (ReflectEvent, GroundingEvent);
the causal chain is built after the fact from the run's events.
Common gotchas¶
| Symptom | Likely cause |
|---|---|
| Reflexion loops forever | The model can't agree with itself. Cap with MaxIterations in your termination condition. |
| Grounding flags everything as unsupported | The judge model is stricter than the answerer. Use the same model for both, or lower the threshold. |
| Causal graph has many disconnected nodes | The model isn't naming entities consistently across turns. Sharpen the system prompt to name entities the same way each time. |
| Reasoning add-ons feel slow | They're extra model calls — that's the trade. Keep them for runs that ship to a human, drop them for hot paths. |
Source and notebook¶
notebook_36_reasoning_patterns.py— all three add-ons end-to-end.tulip.reasoning.reflexiontulip.reasoning.groundingtulip.reasoning.causalReflectNodein the ReAct loop — where reflection plugs in.
Reflexion: Shinn et al., 2023. Typed grounding: see GSAR for the typed-evidence variant the SDK also ships.
See also¶
- GSAR — typed-grounding layer with weighted scoring and tiered replanning.
- Events —
ReflectEvent,GroundingEvent. - Termination — combine
ConfidenceMetwith reflexion to early-stop on high-confidence answers.