# Context Graphs

Context graphs solve a fundamental problem with LLM agents: they have no memory of their own reasoning. Without tracing, an agent makes a decision, returns a response, and everything about how it got there is lost.

KOAN's context graph system traces every agent decision into a property graph database. This creates an audit trail that supports precedent search, policy governance, gap detection, and causal chain analysis.

## Why Context Graphs?

### 1. Audit Trail
Every decision, tool call, and outcome is a graph node. When an agent approves a loan, escalates a ticket, or denies access, you can trace exactly why -- which tools it called, what confidence it had, what reasoning it used. This is critical for regulated industries.

### 2. Precedent Search
Before making a new decision, an agent can query the graph: "Have I seen this entity before? What did I decide last time? Did it work?" This is the `PrecedentSearch` API -- it finds past decisions for the same entity/type and returns their confidence and outcome status. Agents can cite prior decisions to justify new ones.

### 3. Consistency Across Runs
Without context graphs, two identical queries might get different handling because the agent has no institutional memory. With the graph, an agent processing "Alice's PTO request" can see that a previous run already looked up Alice and resolved successfully -- and follow the same approach.

### 4. Policy Governance
Decisions can be linked to `Policy` nodes via `GOVERNED_BY` edges. The `detect_policy_gaps()` query finds decisions that should have been governed by policy but weren't -- surfacing compliance risks automatically.

### 5. Cross-Agent Visibility
In multi-agent systems, each agent writes to the same graph. The finance agent and HR agent both reference entity "alice" -- the graph connects them. You can query "show me every decision any agent made involving this customer" across all agents and runs.

### 6. Debugging & Improvement
When an agent produces a bad outcome, you walk the causal chain backwards from the `Outcome` node through `Decision` nodes to find where it went wrong. Was it a bad classification? Wrong tool? Low confidence that should have triggered escalation?

### The Key Insight
Traditional agent frameworks treat each run as stateless. Context graphs make the agent's decision history a queryable, persistent knowledge base -- the agent doesn't just *do* things, it *remembers* doing them and can learn from its own history.

## Supported Databases

| Database | Protocol | Install Extra |
|----------|----------|---------------|
| Neo4j | Bolt | `koan[graph-neo4j]` |
| Memgraph | Bolt | `koan[graph-memgraph]` |
| FalkorDB | Redis | `koan[graph-falkordb]` |

## Setup

Start a graph database with Docker:

```bash
# FalkorDB (recommended for development)
docker compose -f docker/docker-compose.graph.yml --profile falkordb up -d

# Neo4j
docker compose -f docker/docker-compose.graph.yml --profile neo4j up -d
```

## Graph Client

The unified `GraphClient` abstracts the database differences:

```python
from koan.graph import GraphConfig, create_graph_client

# FalkorDB
config = GraphConfig(provider="falkordb", uri="redis://localhost:6379", database="my_graph")

# Neo4j
config = GraphConfig(provider="neo4j", uri="bolt://localhost:7687", username="neo4j", password="password")

async with create_graph_client(config) as client:
    await client.execute("MATCH (n) RETURN n LIMIT 10")
    await client.execute_write("CREATE (n:Test {id: $id})", {"id": "1"})
    healthy = await client.health_check()
```

## Graph Schema

The decision graph uses this node/relationship structure:

```
Agent -EXECUTED-> Run -CONTAINS-> Decision -LED_TO-> Action
                   |                  |
                   +-PRODUCED-> Outcome  |-GOVERNED_BY-> Policy
                   |                     |-REFERENCED-> Entity
                   +-HAS_FEEDBACK-> Feedback  +-CITED_PRECEDENT-> Decision
```

### Node Types

| Node | Key Properties |
|------|---------------|
| `Agent` | `agent_id` |
| `Run` | `run_id`, `thread_id`, `started_at`, `ended_at`, `status` |
| `Decision` | `decision_id`, `description`, `decision_type`, `confidence`, `reasoning`, `timestamp` |
| `Action` | `action_id`, `type`, `tool_name`, `latency_ms`, `error` |
| `Outcome` | `outcome_id`, `status`, `evaluation_score` |
| `Entity` | `entity_id`, `entity_type`, `source_system` |
| `Policy` | `policy_id`, `name`, `version`, `description` |
| `Feedback` | `feedback_id`, `feedback_type`, `score`, `comment`, `timestamp` |

## Decision Tracer

`DecisionTracer` is the primary API for recording decision traces:

```python
from koan.graph import DecisionTracer, GraphConfig, create_graph_client

config = GraphConfig(provider="falkordb", uri="redis://localhost:6379")

async with create_graph_client(config) as graph:
    async with DecisionTracer(graph, run_id="r-1", agent_id="my-agent", thread_id="t-1") as tracer:
        # Record a decision
        decision_id = await tracer.record_decision(
            description="Classified request as billing inquiry",
            decision_type="classification",
            confidence=0.95,
            reasoning="Keywords: invoice, payment",
        )

        # Record the action taken
        await tracer.record_action(
            decision_id=decision_id,
            action_type="tool_call",
            tool_name="lookup_billing",
            latency_ms=42,
        )

        # Link business entities
        await tracer.link_entity(
            decision_id=decision_id,
            entity_id="acct-001",
            entity_type="account",
            source_system="crm",
        )

        # Record the outcome
        await tracer.record_outcome(
            status="success",
            result="Resolved billing inquiry",
            evaluation_score=0.9,
        )
```

The tracer automatically:
- Creates the `Agent` and `Run` nodes on `__aenter__`
- Sets `Run.status` to `completed` (or `failed` on exception) on `__aexit__`

### Policy Governance

Link decisions to policies for compliance tracking:

```python
decision_id = await tracer.record_decision(
    description="Approved data access",
    decision_type="policy_eval",
    confidence=1.0,
    reasoning="User has admin role",
    policy_id="pol-data-access",  # Links via GOVERNED_BY
)
```

### Precedent Citations

Reference past decisions as precedent:

```python
decision_id = await tracer.record_decision(
    description="Same approach as previous billing case",
    decision_type="tool_selection",
    confidence=0.9,
    reasoning="Citing prior successful resolution",
    precedent_decision_id="prev-decision-uuid",  # Links via CITED_PRECEDENT
)
```

## Querying the Graph

### Decision Trace

Retrieve the full decision trace for a run:

```python
from koan.graph import get_run_trace

trace = await get_run_trace(graph, run_id="r-1")
for record in trace:
    d = record.data
    print(f"[{d['decision_type']}] {d['description']}")
    if d.get("tool_name"):
        print(f"  -> Tool: {d['tool_name']}")
```

### Precedent Search

Find past decisions for the same entity:

```python
from koan.graph import PrecedentSearch

search = PrecedentSearch(graph)
precedents = await search.find(
    entity_id="acct-001",
    decision_type="classification",
    min_confidence=0.8,
    limit=5,
)
for p in precedents:
    print(f"[{p.decision_id[:8]}] {p.description} (confidence: {p.confidence})")
```

### Policy Gap Detection

Find decisions that should have policy governance but don't:

```python
from koan.graph import detect_policy_gaps

gaps = await detect_policy_gaps(graph)
for record in gaps:
    d = record.data
    print(f"[GAP] {d['description']} (type: {d['decision_type']})")
```

### Causal Chain

Walk backwards from an outcome to find the decision chain:

```python
from koan.graph import trace_causal_chain

chain = await trace_causal_chain(graph, outcome_id="o-1")
```

### Cross-Entity Decisions

Find decisions that reference multiple entities:

```python
from koan.graph import find_cross_entity_decisions

decisions = await find_cross_entity_decisions(graph, entity_a="alice", entity_b="team-alpha")
```

### Custom Cypher Queries

Run arbitrary Cypher for custom analysis:

```python
# All agents and their run counts
results = await graph.execute("""
    MATCH (a:Agent)-[:EXECUTED]->(r:Run)
    RETURN a.agent_id AS agent, count(r) AS runs
""")

# Run outcomes by thread
results = await graph.execute("""
    MATCH (a:Agent)-[:EXECUTED]->(r:Run)-[:PRODUCED]->(o:Outcome)
    RETURN a.agent_id AS agent, r.thread_id AS thread, o.status AS status
    ORDER BY r.started_at
""")
```

## Middleware

Auto-instrument any agent with the `traced_agent` wrapper:

```python
from koan.graph import traced_agent

traced = traced_agent(my_agent, graph_client=graph, thread_id="t-1")
result = await traced.run("What is the weather?")
# Decision trace is automatically recorded
```

## Graph-Augmented Reasoning

The `GraphAugmentedAgent` closes the read-write loop -- it reads from the graph before reasoning and writes back after:

```python
from koan.graph import GraphAugmentedAgent, GraphConfig, create_graph_client
from koan.agents import AgentConfig
from koan.llm.client import LLMClient

async with create_graph_client(config) as graph, LLMClient() as llm:
    agent = GraphAugmentedAgent(
        "billing-agent",
        client=llm,
        config=AgentConfig(
            provider="openai",
            model="gpt-4o",
            system_prompt="You are a billing support agent.",
        ),
        tools=registry,
        graph_client=graph,
        entity_ids=["acct-001"],       # Fetch history for these entities
        decision_type="classification", # Filter precedents to this type
    )

    result = await agent.run("Check billing for acct-001")
    # The agent saw: domain profile, entity history, precedents, policies
    # The agent wrote: new decision trace back to the graph
```

Before each run, the agent receives a context block like:

```
--- GRAPH CONTEXT (from past agent decisions) ---
[Domain Profile] You have made 50 past decisions. Decision types: {'classification': 30, 'routing': 20}. Outcome summary: {'success': 45, 'failure': 5}.
[Active Policies]
  - Credit Limit Policy: Credits over $100 require supervisor approval.
[Entity History]
  - account 'acct-001': 3 prior decision(s), last outcome: success
[Relevant Precedents]
  - [success] Classified as billing inquiry (confidence: 0.95, tool: lookup_billing)
--- END GRAPH CONTEXT ---
```

### GraphContextProvider

Use `GraphContextProvider` directly for custom integration:

```python
from koan.graph import GraphContextProvider

provider = GraphContextProvider(graph_client)
ctx = await provider.gather(
    agent_id="my-agent",
    entity_ids=["acct-001", "acct-002"],
    decision_type="classification",
)

# Inject into any prompt
prompt = "You are an agent." + ctx.to_prompt()
```

## Human Feedback and Confidence Calibration

User satisfaction signals and implicit dissatisfaction (fork/rewind events) feed back into the graph to calibrate agent confidence over time.

### Recording Feedback

```python
from koan.graph import FeedbackType, record_feedback

# Explicit: "Were you satisfied with this answer?"
await record_feedback(
    graph_client,
    run_id="run-123",
    feedback_type=FeedbackType.THUMBS_UP,
    score=1.0,
    comment="Very helpful!",
)

# Or use the agent's convenience method
result = await agent.run("Check my balance")
await agent.submit_feedback(satisfied=True, comment="Accurate answer")
```

### Fork/Rewind as Dissatisfaction Signals

When a user forks or rewinds a conversation, it implies dissatisfaction. `ForkableThread` fires an `on_fork` callback that can record this to the graph:

```python
from koan.graph import FeedbackType, record_feedback
from koan.memory.fork import ForkableThread

async def on_fork(thread_id: str, event_type: str, checkpoint: str) -> None:
    ftype = FeedbackType.FORK if event_type == "fork" else FeedbackType.REWIND
    await record_feedback(
        graph_client,
        run_id=current_run_id,
        feedback_type=ftype,
        score=-0.5,  # Implicit negative signal
    )

thread = ForkableThread("t1", on_fork=on_fork)
```

### Confidence Calibration

`ConfidenceCalibrator` queries past feedback to compute an adjustment factor:

```python
from koan.graph import ConfidenceCalibrator

calibrator = ConfidenceCalibrator(graph_client)
cal = await calibrator.get_calibration(
    agent_id="billing-agent",
    decision_type="classification",
)

print(f"Factor: {cal.factor}")          # 0.85 = 85% positive feedback
print(f"Samples: {cal.sample_count}")   # 20 feedback signals
print(f"Positive: {cal.positive_rate}") # 0.85
print(f"Fork rate: {cal.fork_rate}")    # 0.10

# Use to adjust raw confidence
calibrated_confidence = raw_confidence * cal.factor
```

The calibrator is automatically integrated into `GraphAugmentedAgent` -- when feedback history exists, the agent sees it in its context:

```
[Feedback History]
  Past feedback: 15 ratings, 80% satisfaction, avg score: 0.6
  Recent comments: Great work!; Could be better
```

### Calibration Algorithm

- Fork/rewind signals are weighted at **0.7x** of explicit negative feedback (they're implicit, not definitive)
- The calibration factor has a **floor of 0.3** to prevent total confidence collapse
- Formula: `factor = max(0.3, 1.0 - (weighted_negatives / total * 0.5))`

## Self-Organizing Policies

Policies can emerge automatically from repeated tool usage patterns -- no manual seeding required. The `PolicyManager` watches for patterns and promotes them when they reach sufficient frequency and success rate.

### How It Works

The self-organizing lifecycle has five phases:

1. **Cold Start** — No policies exist. All tools are available. The agent operates freely.
2. **Accumulation** — Each run records tool actions to the graph. `auto_evolve()` scans for repeating patterns.
3. **Emergence** — When a tool pattern repeats 3+ times with >70% success, it gets promoted to a `Policy` node.
4. **Gating** — `PolicyGatedTools` filters the agent's available tools to only those allowed by active policies. Tools outside the policy set are structurally blocked.
5. **Retirement** — `retire_underperforming()` checks governed decisions. If >50% have failed outcomes, the policy is deleted and all tools become available again.

```python
from koan.graph import GraphAugmentedAgent

# The agent handles the full lifecycle automatically:
# - Gathers context (including active policies) before each run
# - Gates tools based on policies
# - Records decision traces after each run
# - Calls auto_evolve() and retire_underperforming() after each run
agent = GraphAugmentedAgent(
    "billing-agent",
    client=llm_client,
    config=agent_config,
    tools=registry,
    graph_client=graph,
    entity_ids=["acct-001"],
    decision_type="billing",
)
```

### PolicyManager API

```python
from koan.graph.policies import PolicyManager

pm = PolicyManager(graph_client)

# Discover and promote patterns
promoted = await pm.auto_evolve()
# Returns list of newly created policy IDs

# Retire policies with high failure rates
retired = await pm.retire_underperforming()
# Returns list of deleted policy names

# Link a decision to matching policies (creates GOVERNED_BY edges)
linked = await pm.link_decision_to_matching_policies(
    decision_id="d-123",
    decision_type="billing",
)
```

### PolicyGatedTools

Structurally enforces which tools an agent can use:

```python
from koan.graph.gated_tools import PolicyGatedTools

# Extract allowed tools from policies
allowed = PolicyGatedTools.extract_tools_from_policies(active_policies)
# Returns: ["check_balance"] (only tools mentioned in policy rules)

# Wrap the tool provider
gated = PolicyGatedTools(registry, allowed_tools=["check_balance"])
gated.definitions()  # Only returns check_balance definition
await gated.execute("apply_credit", {"amount": 10})  # Raises ToolExecutionError
```

### Testing the Lifecycle

Demo 5 (`demos/demo5_self_organizing.py`) runs the full lifecycle automatically in 9 steps. Demos 6-8 let you observe emergence interactively -- repeat the same query pattern 3+ times and watch policies appear in the info panel.

## Feedback-Weighted Precedents

Precedent queries JOIN to Feedback nodes so each past decision carries its feedback score. This lets the LLM distinguish good decisions from bad ones.

### How It Appears in Context

```
[Relevant Precedents — learn from successful decisions, avoid patterns from failed ones]
  - [success, thumbs_up (+1.0)] Q: Look up case-2024-001 → A: Found civil case..., tool: lookup_case
  - [success] Q: Check documents for case-2024-001 → A: 4 documents found..., tool: search_documents
  - [failure, thumbs_down (-1.0)] Q: Draft motion without lookup → A: Cannot draft..., tool: draft_motion
```

Precedents are sorted by quality: successful + liked decisions appear first, failed + disliked decisions appear last. The LLM sees proven patterns as exemplars and bad patterns as warnings.

### Sorting Logic

Each precedent is ranked by `(outcome_rank, feedback_score)`:
- **outcome_rank**: success=2, unknown=1, failure=0
- **feedback_score**: from -1.0 (thumbs down) to +1.0 (thumbs up), 0 if no feedback

This means a successful decision with thumbs-up always outranks a successful one with no feedback, which outranks a failed one with thumbs-down.

## Visual Exploration

FalkorDB includes a browser UI for interactive graph visualization:

```bash
# Included in the falkordb profile
docker compose -f docker/docker-compose.graph.yml --profile falkordb up -d
# Open http://localhost:3000
# Connect to host: falkordb, port: 6379
# Select your graph and run: MATCH (n)-[r]->(m) RETURN n, r, m
```
