Trace every agent decision, score what mattered, explore what-if alternatives, flag what needs human review. A structured explainability library for AI agent systems.
When an AI agent makes a decision β approve a deployment, route a ticket, trigger an alert β you need to know why. Cocapn-explain captures the full decision lifecycle: what went in, how the agent reasoned step by step, what came out, and whether the outcome matched expectations. It then analyses that trace to produce feature importance scores, counterfactual "what if" scenarios, and human-readable explanations.
The library gives you:
- Decision traces β record every step (observe β reason β decide β act β verify) with inputs, outputs, confidence, and timing
- Decision objects β structured records of agent decisions with inputs, reasoning steps, outputs, status, and confidence
- Feature importance β score and rank which inputs most influenced a decision (manual, weight-based, or perturbation analysis)
- Counterfactuals β "what if X had been different?" exploration: single-feature changes, grid search, boundary detection
- Explainer β combines traces + importance + counterfactuals into human-readable narrative explanations (markdown or JSON)
- Oversight queue β automatically triage decisions by priority (must-review, sampled, logged) based on confidence and risk
- Explanation reports β comprehensive auditable reports combining all analysis with confidence, risk assessment, and review workflow
Explainability isn't one thing β it's a pipeline:
Decision Trace β Feature Importance β Counterfactuals β Narrative β Oversight β Report
Each layer adds a different kind of understanding:
- Trace answers "what happened?"
- Importance answers "what mattered?"
- Counterfactuals answer "what would change the outcome?"
- Narrative answers "why did the agent do this?" in human language
- Oversight answers "does a human need to look at this?"
- Report packages everything for audit
pip install cocapn-explainRequires Python β₯ 3.10. No external dependencies.
from cocapn_explain import ExplainTrace, StepType
trace = ExplainTrace(agent_id="deploy-bot", task="deploy v2.3.1")
trace.add_step(StepType.OBSERVE, "Received deployment request", confidence=0.9,
inputs={"version": "v2.3.1", "environment": "staging"})
trace.add_step(StepType.REASON, "All CI checks pass", confidence=0.95,
inputs={"tests_passed": 47, "lint_clean": True})
trace.add_step(StepType.DECIDE, "Approve deployment", confidence=0.88)
trace.add_step(StepType.ACT, "Deployed to staging", confidence=0.85,
outputs={"deploy_id": "d-12345"})
trace.add_step(StepType.VERIFY, "Health check passed", confidence=0.92)
print(trace.summarize())from cocapn_explain import FeatureImportance, Decision, DecisionInput
# Build a decision
decision = Decision(agent_id="deploy-bot", action="deploy", confidence=0.88)
decision.add_input("ci_status", "passing", source="github", weight=0.9)
decision.add_input("test_coverage", 0.87, source="pytest", weight=0.6)
decision.add_input("time_since_last_deploy", "2h", source="scheduler", weight=0.3)
decision.add_input("rollback_count", 0, source="history", weight=0.8)
# Auto-score from weights
fi = FeatureImportance()
fi.score_from_weights(decision.weighted_inputs, decision.confidence)
print(fi.summarize())
# Feature Importance:
# 1. ci_status: 0.43 + βββββββββ ββββ [critical]
# 2. rollback_count: 0.38 + ββββββββ [high]
# 3. test_coverage: 0.29 + ββββββ [moderate]
# 4. time_since_last_deploy: 0.14 + βββ [low]from cocapn_explain import FeatureImportance
# Score by removing each feature and measuring confidence drop
def mock_decide(features):
# Your actual decision function
if features.get("ci_status") != "passing":
return 0.2 # low confidence without CI
return 0.88
fi = FeatureImportance()
fi.perturbation_analysis(
features={"ci_status": "passing", "test_coverage": 0.87, "rollback_count": 0},
decide_fn=mock_decide,
baseline_confidence=0.88,
)
# Removing "ci_status" drops confidence by 0.68 β CRITICAL importancefrom cocapn_explain import CounterfactualGenerator
def decide_fn(features):
if features.get("ci_status") != "passing":
return ("reject", 0.3)
return ("approve", 0.88)
cf_gen = CounterfactualGenerator(decide_fn=decide_fn)
features = {"ci_status": "passing", "test_coverage": 0.87, "rollback_count": 0}
# Single counterfactual
cf = cf_gen.generate_single(features, "ci_status", "failing")
print(cf.description) # "If 'ci_status' were 'failing' instead of 'passing'"
print(cf.predicted_outcome) # "reject"
print(cf.predicted_confidence) # 0.3
print(cf.would_change_decision) # True
# Grid of counterfactuals
cfs = cf_gen.generate_grid(features, {
"ci_status": ["failing", "error", "unknown"],
"test_coverage": [0.5, 0.6, 0.7],
})
for cf in cfs:
if cf.would_change_decision:
print(f" {cf.description} β {cf.predicted_outcome}")from cocapn_explain import Explainer, Decision, FeatureImportance, CounterfactualGenerator
decision = Decision(agent_id="deploy-bot", action="deploy", confidence=0.88)
decision.add_input("ci_status", "passing", weight=0.9)
decision.add_reasoning("CI checks all passing")
decision.add_reasoning("Test coverage above threshold (87%)")
decision.add_reasoning("No rollbacks in recent history")
decision.add_output("deploy_id", "d-12345")
explainer = Explainer(counterfactual_generator=cf_gen)
explanation = explainer.explain_decision(decision, feature_importance=fi, counterfactuals=cfs)
# Markdown output
print(explanation.to_markdown())
# ## Why the agent decided to 'deploy'
#
# **Confidence:** 88%
#
# The agent decided to 'deploy' through 3 reasoning step(s), with 88% confidence.
#
# ### Reasoning
# - CI checks all passing
# - Test coverage above threshold (87%)
# - No rollbacks in recent history
#
# ### Key Factors
# - 'ci_status' (supported, importance=0.43) β ...
#
# ### Alternatives Considered
# - If 'ci_status' were 'failing' instead of 'passing' β would result in 'reject'from cocapn_explain import Explainer
explainer = Explainer()
explanation = explainer.quick_explain(
action="restart_service",
confidence=0.72,
reasoning=["Memory usage > 95%", "Last restart was 6h ago"],
key_factors=["memory_usage", "uptime"],
)
print(explanation.summary)from cocapn_explain import OversightQueue, DecisionTrace
queue = OversightQueue(p1_sample_rate=0.1)
# Enqueue traces β auto-classified by risk/confidence
queue.enqueue(DecisionTrace(
agent_id="deploy-bot",
decision="deploy to production",
reasoning="CI passed, low risk",
confidence=0.45, # Low β P0 (must review)
risk_level="CRITICAL",
))
queue.enqueue(DecisionTrace(
agent_id="monitor",
decision="scale up pods",
reasoning="CPU > 80%",
confidence=0.92, # High β P2 (logged)
risk_level="LOW",
))
# Get review queue (all P0 + sampled P1)
for item in queue.get_review_queue():
print(f"[{item.priority.name}] {item.trace.decision} "
f"(conf={item.trace.confidence:.2f}, age={item.age_seconds:.0f}s)")
# Review a decision
queue.review("deploy to production", approved=True)from cocapn_explain import ExplanationReport, Explainer, Decision
decision = Decision(agent_id="deploy-bot", action="deploy", confidence=0.88)
# ... add inputs, reasoning, outputs ...
report = ExplanationReport(
decision=decision,
explanation=explanation,
feature_importance=fi,
counterfactuals=cfs,
tags=["deployment", "staging"],
)
print(f"Needs review: {report.needs_human_review}") # True if low confidence / unexpected outputs
print(f"Risk: {report.risk_assessment}") # LOW / MODERATE / HIGH / CRITICAL
print(f"Confidence: {report.confidence_score:.0%}") # Adjusted for unexpected outputs
# Full markdown report
print(report.to_markdown())
# Approve and archive
report.approve(reviewer="alice", notes="Looks good")| Class | Description |
|---|---|
ExplainTrace(agent_id, task) |
Complete trace of an agent decision process |
ExplainTrace.add_step(step_type, description, ...) |
Record a step (OBSERVE, REASON, DECIDE, ACT, VERIFY) |
ExplainTrace.summarize() |
Human-readable trace summary |
TraceStep |
Single step with type, description, confidence, timing, I/O |
DecisionTrace(agent_id, decision, reasoning, confidence, ...) |
Simplified trace for quick human review |
StepType |
Enum: OBSERVE, REASON, DECIDE, ACT, VERIFY |
| Class | Description |
|---|---|
Decision(agent_id, action) |
Complete structured decision record |
.add_input(name, value, source, weight) |
Add a weighted input factor |
.add_reasoning(step) |
Add a reasoning step |
.add_output(name, value, expected) |
Add an output (with expected flag) |
.finalize(status) |
Mark as APPROVED / REJECTED / ROLLED_BACK |
.weighted_inputs |
Dict of {name: (value, weight)} |
.unexpected_outputs |
Outputs that didn't match expectations |
.summarize() |
Human-readable decision summary |
DecisionInput |
Single input factor with name, value, source, weight |
DecisionOutput |
Single output with name, value, expected flag |
DecisionStatus |
Enum: PENDING, APPROVED, REJECTED, ROLLED_BACK |
| Class / Method | Description |
|---|---|
FeatureImportance() |
Score and rank features by influence |
.add_feature(name, score, contribution, direction) |
Manual scoring |
.score_from_weights(weighted_inputs, confidence) |
Auto-score from DecisionInput weights |
.perturbation_analysis(features, decide_fn, baseline) |
Score by removing each feature |
.top_features |
Top 3 features by score |
.critical_features |
Features with score β₯ 0.8 |
.summarize() |
ASCII bar chart + importance levels |
FeatureScore |
Single feature score with rank, direction, level |
ImportanceLevel |
Enum: CRITICAL, HIGH, MODERATE, LOW, NEGLIGIBLE |
| Class / Method | Description |
|---|---|
CounterfactualGenerator(decide_fn) |
Generate "what if" scenarios |
.generate_single(features, name, new_value) |
One counterfactual by changing one feature |
.generate_grid(features, perturbations) |
Multiple features Γ multiple values |
.find_decision_boundary(features, name, test_values, threshold) |
Find the value that flips the decision |
.summarize(counterfactuals) |
Human-readable summary |
Counterfactual |
Single scenario: description, changes, predicted outcome, feasibility |
| Class / Method | Description |
|---|---|
Explainer(counterfactual_generator) |
Generate explanations from decision data |
.explain_decision(decision, feature_importance, counterfactuals) |
Full explanation |
.explain_with_counterfactuals(decision, features, perturbations, ...) |
Explanation + auto counterfactuals |
.quick_explain(action, confidence, reasoning, key_factors) |
One-shot without Decision object |
Explanation |
Human-readable explanation: title, summary, reasoning, key_factors, alternatives, caveats |
Explanation.to_markdown() |
Render as markdown |
Explanation.to_dict() |
Render as dict |
| Class / Method | Description |
|---|---|
OversightQueue(p1_sample_rate=0.1) |
Priority queue for human review |
.enqueue(trace) |
Auto-classify and enqueue (P0/P1/P2) |
.get_review_queue() |
Items needing review (all P0 + sampled P1) |
.review(trace_id, approved) |
Mark as reviewed |
.stats |
Dict of pending/reviewed counts per priority |
OversightPriority |
Enum: P0_MUST_REVIEW, P1_SAMPLED, P2_LOGGED |
| Class / Method | Description |
|---|---|
ExplanationReport(decision, explanation, ...) |
Comprehensive auditable report |
.needs_human_review |
True if low confidence, unexpected outputs, or rolled back |
.confidence_score |
Aggregate confidence (penalised for unexpected outputs) |
.risk_assessment |
LOW / MODERATE / HIGH / CRITICAL |
.flag(reason) |
Flag for human attention |
.approve(reviewer, notes) |
Approve and finalise |
.archive() |
Archive the report |
.to_markdown() |
Full markdown report |
.to_dict() |
Full dict serialisation |
ReportStatus |
Enum: DRAFT, FINAL, FLAGGED, ARCHIVED |
βββββββββββββββ
β Agent makes β
β a decision β
ββββββββ¬βββββββ
β
βΌ
ββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β trace.py ββββββΆβ feature.py ββββββΆβ counterfactual β
β β β β β .py β
β ExplainTrace β β FeatureImportanceβ β Counterfactual β
β TraceStep β β FeatureScore β β Counterfactual β
β DecisionTraceβ β perturbation β β Generator β
β StepType β β score_from_ β β decision_boundaryβ
β β β weights β β grid search β
ββββββββββββββββ ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ
β β β
β βββββββββββββ¬ββββββββββββββ
β β
β ββββββββββΌβββββββββ
β β explainer.py β
β β β
β β Explainer β
β β Explanation β
β β .to_markdown() β
β ββββββββββ¬βββββββββ
β β
βΌ βΌ
ββββββββββββββββ ββββββββββββββββββββ
β oversight.py β β report.py β
β β β β
β OversightQ β β ExplanationReportβ
β Priority P0 βββββββββββββββββ .needs_review β
β P1, P2 β β .risk_assessment β
β Auto-triage β β .flag() / . β
β β β approve() β
ββββββββββββββββ ββββββββββββββββββββ
β
βΌ
Human reviewer
When a DecisionTrace is enqueued, it's auto-classified:
| Condition | Priority |
|---|---|
risk_level == "CRITICAL" or confidence < 0.3 |
P0 β Must review before action |
risk_level == "HIGH" or confidence < 0.5 |
P1 β Sampled (default 10%) |
| Everything else | P2 β Logged, review only if outcome bad |
| Score | Level | Meaning |
|---|---|---|
| β₯ 0.8 | CRITICAL | Decision would change without this feature |
| β₯ 0.6 | HIGH | Major influence |
| β₯ 0.4 | MODERATE | Notable influence |
| β₯ 0.2 | LOW | Minor influence |
| < 0.2 | NEGLIGIBLE | Barely affects outcome |
| Adjusted confidence | Risk |
|---|---|
| β₯ 0.9 | LOW |
| β₯ 0.7 | MODERATE |
| β₯ 0.5 | HIGH |
| < 0.5 | CRITICAL |
Adjusted confidence penalises for unexpected outputs (up to β0.3).
pip install pytest
pytest tests/ -v71 tests across test files covering every module.
MIT