Skip to content

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

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.
  • EventsReflectEvent, GroundingEvent.
  • Termination — combine ConfidenceMet with reflexion to early-stop on high-confidence answers.