Cogitator
Advanced

Causal Reasoning

Build causal graphs, run d-separation analysis, perform counterfactual reasoning, and predict intervention effects.

Overview

The CausalReasoner lets agents understand cause-and-effect relationships. Instead of relying on correlations, agents can build directed acyclic graphs (DAGs) that model causal structure, then use those graphs to predict effects of actions, explain outcomes, and reason about counterfactuals.

import { CausalReasoner } from '@cogitator-ai/core';

const reasoner = new CausalReasoner({
  llmBackend: backend,
  model: 'gpt-6.1-sol', // sent to the backend as is (this is the default)
  config: {
    enableLLMDiscovery: true,
    enableSafetyChecks: true,
    minEdgeConfidence: 0.6,
  },
});

The graph-level tools — the builder, d-separation, adjustment sets, the inference engine and counterfactuals — are pure computations without model calls. The CausalReasoner adds LLM-backed prediction, explanation, planning and discovery on top, keeping one causal graph per agent id.

Building Causal Graphs

Use CausalGraphBuilder to construct a graph with a fluent API. Nodes represent variables, edges represent causal relationships.

import { CausalGraphBuilder } from '@cogitator-ai/core';

const graph = CausalGraphBuilder.create('user-engagement')
  .treatment('marketing_spend', 'Marketing Spend')
  .mediator('site_traffic', 'Site Traffic')
  .confounder('seasonality', 'Seasonality')
  .outcome('revenue', 'Revenue')
  .from('marketing_spend')
  .causes('site_traffic', { strength: 0.8, confidence: 0.9 })
  .from('site_traffic')
  .causes('revenue', { strength: 0.7 })
  .from('seasonality')
  .causes('site_traffic', { strength: 0.4 })
  .causes('revenue', { strength: 0.3 })
  .build();

The builder supports typed variable roles — variable (observed by default), treatment, outcome, confounder, mediator, instrumental, collider and latent — and the relation types causes, enables, prevents, mediates, confounds and moderates. Edges default to strength and confidence 1. connect(source, target, relationType, options) adds an edge without moving the cursor, and withDomain() describes a variable's values. build() rejects self-loops and duplicate edges.

You can also attach structural equations to nodes for quantitative counterfactual analysis:

const linearGraph = CausalGraphBuilder.create('linear-model')
  .variable('X', 'Treatment')
  .withEquation({ type: 'linear', intercept: 0, coefficients: {} })
  .variable('Y', 'Outcome')
  .withEquation({ type: 'linear', intercept: 2, coefficients: { X: 0.5 } })
  .from('X')
  .causes('Y')
  .build();

D-Separation

D-separation determines whether two variables are conditionally independent given a set of observed variables. This is essential for identifying valid adjustment sets and testing causal assumptions.

import { dSeparation, findMinimalSeparatingSet } from '@cogitator-ai/core';

const result = dSeparation(graph, 'marketing_spend', 'revenue', ['seasonality']);

console.log(result.separated); // true if conditionally independent
console.log(result.openPaths); // paths that remain unblocked
console.log(result.blockedPaths); // paths blocked by conditioning set

const minimal = findMinimalSeparatingSet(graph, 'marketing_spend', 'revenue'); // string[] | null

x and y may also be arrays of variables. findBackdoorAdjustment, findFrontdoorAdjustment and findAllAdjustmentSets return AdjustmentSets (variables, type, formula).

The algorithm handles all three types of triples -- chains, forks, and colliders -- following the standard rules: conditioning on a chain or fork node blocks the path, while conditioning on a collider (or its descendants) opens it.

Causal Inference Engine

The CausalInferenceEngine provides higher-level inference operations built on top of d-separation and adjustment formulas.

import { CausalInferenceEngine } from '@cogitator-ai/core';

const engine = new CausalInferenceEngine(graph);

const identifiable = engine.isIdentifiable('marketing_spend', 'revenue');
// { identifiable: true, reason: 'Identifiable via backdoor criterion', adjustmentSet: ... }

const observedData = {
  marketing_spend: [1, 0, 1, 1, 0],
  seasonality: [1, 1, 0, 0, 1],
  revenue: [520, 410, 480, 505, 395],
};
const ate = engine.estimateATE('marketing_spend', 'revenue', observedData);
// { effect, standardError, confidenceInterval, adjustmentSet, isIdentifiable, formula }

const independence = engine.checkConditionalIndependence('site_traffic', 'seasonality', [
  'revenue',
]);
// { independent, paths, openPaths }

const instruments = engine.findInstrumentalVariables('marketing_spend', 'revenue');
const adjustmentSets = engine.getAllAdjustmentSets('marketing_spend', 'revenue');

The engine searches for a backdoor adjustment set first, then a frontdoor one, and reports the identification formula. estimateATE treats the treatment as binary (rows where it is 1 against the rest) and adjusts for the set's variables in the data; estimateATT estimates the effect on the treated. When the effect is not identifiable or the data lacks a column, effect is NaN. computeCounterfactual(query) runs the counterfactual reasoner below on the engine's graph (unless config.enableCounterfactual is false).

Counterfactual Reasoning

Counterfactuals answer "what if?" questions using Pearl's three-step process: abduction (infer noise from observations), action (apply intervention), and prediction (compute outcome in the modified world). They need structural equations on the nodes, like linearGraph above:

import { CounterfactualReasoner } from '@cogitator-ai/core';

const cfReasoner = new CounterfactualReasoner({
  defaultNoiseStd: 0.5,
  maxIterations: 100,
});

const result = cfReasoner.evaluate(linearGraph, {
  factual: { X: 1, Y: 2.5 },
  intervention: { X: 3 },
  target: 'Y',
  question: 'What would Y have been with X = 3?',
});

console.log(result.factualValue); // 2.5
console.log(result.counterfactualValue); // 3.5
console.log(result.explanation);
// "Given the factual observation where Y=2.5, if we had intervened to set X=3,
//  then Y would have been 3.5."
console.log(result.reasoning); // { abduction, action, prediction }

evaluateCounterfactual(graph, query, options) does the same in one call. Structural equations support linear, logistic, polynomial and custom types with configurable noise distributions (gaussian, uniform, bernoulli). A node without an equation keeps its factual value.

A custom equation is an arithmetic expression over the parent node ids in customFn:

const profitGraph = CausalGraphBuilder.create('profit-model')
  .variable('price', 'Price')
  .variable('demand', 'Demand')
  .outcome('profit', 'Profit')
  .withEquation({ type: 'custom', customFn: '2 * price - log(demand) + max(price, 1)' })
  .from('price')
  .causes('profit')
  .from('demand')
  .causes('profit')
  .build();

const whatIf = cfReasoner.evaluate(profitGraph, {
  factual: { price: 10, demand: 100, profit: 25 },
  intervention: { price: 12 },
  target: 'profit',
  question: 'What would profit have been at price 12?',
});

It may use numbers, parent ids, + - * / ^, parentheses and the functions abs, exp, log, sqrt, pow, min, max, tanh and sigmoid; a parent without a value counts as 0. The expression is parsed, never run as code, so it is safe to load from storage or take from an LLM; an invalid one throws when it is evaluated. As with linear, the node's noise term is added to its value.

Intervention Analysis

The CausalReasoner combines graph-based reasoning with LLM capabilities to predict effects, explain outcomes, and plan interventions. Each method works on the graph of the agent id you pass. That graph starts empty unless the reasoner's graphStore already holds one for the agent, so seed it with what you know:

import { InMemoryCausalGraphStore } from '@cogitator-ai/core';

const graphStore = new InMemoryCausalGraphStore();
await graphStore.save({ ...graph.toData(), metadata: { agentId: 'agent-001' } });

const reasoner = new CausalReasoner({ llmBackend: backend, graphStore });
const effect = await reasoner.predictEffect('increase marketing_spend by 50%', 'agent-001', {
  observedVariables: { revenue: 50000, site_traffic: 5000 },
});
// { effects: [...], sideEffects: [...], confidence: 0.82 }

const explanation = await reasoner.explainCause('revenue', 50000, 'agent-001');

const plan = await reasoner.planForGoal('revenue', 100000, 'agent-001', undefined, {
  forbidden: ['layoffs'],
  required: ['marketing_spend'],
});

More capabilities on the same graph:

  • evaluateToolCall(toolCall, agentId) predicts a tool call's effects and returns { isSafe, riskLevel, warnings, recommendations } (always safe when enableSafetyChecks is false).
  • scoreBranch(branch, agentId) scores a Tree-of-Thought branch by its predicted effects.
  • analyzeErrorCausally(error, trace, agentId) explains a failure from an execution trace.
  • generateHypotheses(agentId, goal?) proposes causal hypotheses from stored patterns; validateHypothesis(hypothesis, context) checks one and adds validated edges to the graph.
  • getGraph(agentId), saveGraph(agentId), loadGraph(agentId) and getStats() manage and inspect the graphs.

Learning from Execution

The reasoner extracts causal structure from agent execution traces with the LLM (when enableLLMDiscovery is on, the default) and refines edge confidences based on intervention outcomes:

await reasoner.learnFromTrace(executionTrace, 'agent-001');

await reasoner.learnFromIntervention(
  { marketing_spend: 200 }, // intervention
  { revenue: 50000 }, // observed before
  { revenue: 65000 }, // observed after
  predictedEffect, // what we expected
  true, // was it successful?
  'agent-001'
);

For every intervened variable and every observed variable whose value changed, an existing edge between them gains 0.1 confidence on success and loses 0.1 on failure (never below 0.1), allowing the causal graph to become more accurate over time. Interventions are kept in the interventionLog.

On this page