Jasmind Documentation

The observation and security layer for production AI agents. Instrument your agent in minutes — no rewrites, no external dependencies.

Five-minute integration. Jasmind activates both Observation and Security from the same three lines of code. Your agent logic stays unchanged.

What Jasmind does

Jasmind sits between your agent code and your LLM provider. Every call is captured, analyzed by two parallel engines, and surfaced in the dashboard:

Pillar What it detects What you get
Observation Loops, long context, repeated tool calls, cost spikes Per-span cost breakdown, waste estimate, concrete optimization suggestions
Security Prompt injection, PII leakage, API secret exposure, risky tool sequences Real-time alerts with severity, redacted evidence, Security Score per trace
🔒
Self-hosted by design. All data stays in your infrastructure. Jasmind runs entirely via Docker Compose — no cloud account required.

Quick Start

Get observation and security running in your agent in under 5 minutes.

  • 1
    Start the Jasmind server
    Clone the repo and run Docker Compose. The collector API starts on :8080, the dashboard on :3000.
  • 2
    Install the SDK
    One package with no required dependencies beyond your existing agent stack.
  • 3
    Wrap your agent
    Add three lines. Both Observation and Security activate automatically.
  • 4
    Open the dashboard
    Go to http://localhost:3000 — traces and security alerts appear in real time as your agent runs.
terminal
# 1. Start the server
git clone https://github.com/jasmind-ai/jasmind
docker-compose up -d

# 2. Install the SDK
pip install jasmind
agent.py
from jasmind import Jasmind, wrap_openai
from openai import OpenAI

# Connect to your local Jasmind instance
jm = Jasmind(endpoint="http://localhost:8080")

# Wrap the OpenAI client — every call is now captured
client = wrap_openai(OpenAI(), tracer=jm)


@jm.trace(name="research_task")
def run_agent(user_input: str) -> str:
    # your agent logic — completely unchanged
    for step in range(10):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": user_input}],
        )
        # ↑ Observation: cost, tokens, latency, loop detection
        # ↑ Security: injection, PII, secrets — scanned in parallel
        ...
    return result

Self-Hosting

Jasmind is designed to run entirely in your own infrastructure. No telemetry is sent anywhere — your agent data stays local.

Docker Compose

docker-compose.yml
version: '3.8'
services:
  backend:
    build: ./backend
    ports:
      - "8080:8080"
    volumes:
      - ./data:/app/data
    environment:
      - DATABASE_URL=sqlite:///./data/jasmind.db

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - VITE_API_URL=http://localhost:8080
terminal
docker-compose up -d

# Collector API → http://localhost:8080
# Dashboard     → http://localhost:3000
ServicePortDescription
Backend (FastAPI)8080Trace collector API + analyzers
Frontend (React)3000Dashboard UI
Database (SQLite)Stored at ./data/jasmind.db, survives container restarts
⚠️
MVP uses SQLite. For production workloads with >1M spans/day, plan for a PostgreSQL migration (schema is compatible).

Python SDK

The Python SDK provides two integration patterns. Both activate Observation and Security simultaneously.

Method A — Decorator

Wrap any function with @jm.trace() to create a trace around its execution. Every LLM call inside the function is automatically captured as a child span.

agent.py
from jasmind import Jasmind

jm = Jasmind(endpoint="http://localhost:8080")

@jm.trace(name="classify_email")
def classify_email(email_body: str) -> str:
    # your function — unchanged
    ...

Method B — OpenAI Client Wrap

Wrap the OpenAI client directly. Every chat.completions.create() call is captured with full prompt, completion, token counts, and cost.

agent.py
from jasmind import Jasmind, wrap_openai
from openai import OpenAI

jm = Jasmind(endpoint="http://localhost:8080")
client = wrap_openai(OpenAI(), tracer=jm)

# Use client exactly as before — nothing else changes
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": prompt}],
)
ParameterTypeDescription
endpointstrURL of your Jasmind collector. Default: http://localhost:8080
namestrHuman-readable trace name. Appears in the dashboard trace list.

OpenAI Proxy Mode

No SDK installation required. Change one line — redirect your OpenAI client's base_url to the Jasmind proxy. All existing code works unchanged.

agent.py
import os
from openai import OpenAI

# Change only base_url — one line
client = OpenAI(
    base_url="http://localhost:8080/proxy/openai",
    api_key=os.environ["OPENAI_API_KEY"],
)

# Everything below stays exactly the same
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": prompt}],
)

Anthropic proxy is also available at /proxy/anthropic/v1/messages.


HTTP API (Language-agnostic)

For non-Python environments, send spans directly via HTTP POST. Any language or runtime that can make HTTP requests works out of the box.

terminal
curl -X POST http://localhost:8080/api/v1/spans \
  -H "Content-Type: application/json" \
  -d '{
    "trace_id":         "abc123",
    "name":             "call_gpt4o",
    "span_type":        "llm",
    "model":            "gpt-4o",
    "prompt_tokens":    1200,
    "completion_tokens": 340,
    "cost_usd":         0.0092,
    "started_at":       "2026-06-27T10:00:00Z",
    "ended_at":         "2026-06-27T10:00:02Z"
  }'

Observation

After each trace completes, the Observation Analyzer runs automatically. It examines every span in the trace, flags anomalies, calculates wasted cost, and generates concrete optimization suggestions.

💡
The Observation Analyzer runs as a background task — it never adds latency to your agent's execution path.

Loop Detection

A loop is flagged when the same span name appears more than 5 times within a single trace. This is the most common cause of runaway token costs in agentic workflows.

analyzers/observation.py
from collections import Counter

def detect_loop(spans):
    name_counts = Counter(s.name for s in spans)
    return any(count > 5 for count in name_counts.values())

When a loop is detected, the trace status is set to loop_detected and an insight is generated with the estimated wasted cost and a suggested fix (e.g., add a max_iterations guard).

Long Context Detection

Spans with more than 8,000 prompt tokens are flagged. The insight suggests switching to RAG, summarization, or a model with a cheaper per-token rate for long inputs.

analyzers/observation.py
LONG_CONTEXT_THRESHOLD = 8000  # prompt_tokens

Repeated Call Detection

When the same tool is called with identical inputs two or more times in one trace, Jasmind flags it as a repeated call and estimates how much would be saved by caching the result.

analyzers/observation.py
# hash(tool_name + tool_input) duplicates within one trace
def detect_repeated_calls(spans):
    seen = set()
    for span in spans:
        key = hash((span.tool_name, str(span.tool_input)))
        if key in seen:
            span.obs_flags.append("repeated_call")
        seen.add(key)

Insights Engine

All flagged spans feed into the Insights Engine, which generates concrete, actionable suggestions with estimated savings and quality-impact labels.

Suggestion TypeTriggerExample
cheaper_model gpt-4o span with prompt_tokens < 2,000 Switch to gpt-4o-mini — 93% cheaper, minimal quality impact
add_caching Repeated tool calls detected Cache web_search result — called 4× with identical input
reduce_context Long context detected (>8K tokens) Switch to RAG — full doc in context adds $0.04/call unnecessarily
loop_guard Loop detected Add max_iterations=3 — agent looped 7× before terminating

Security

The Security Scanner runs in parallel with the Observation Analyzer. It uses a pure rule-based engine — no LLM is used for detection, which means zero added latency and no external API calls.

Every span's prompt_preview and completion_preview are scanned against four detection categories. Each match creates a security_alert record and deducts from the trace's Security Score.

🛡️
Prompt Injection
Critical

Matches instruction-override patterns in any span's prompt text. Fires when a user-provided input attempts to redirect the agent's behavior.

analyzers/security.py
INJECTION_PATTERNS = [
    r"ignore (all |previous |above |prior )?instructions?",
    r"forget (everything|what you were told)",
    r"you are now",
    r"new (system |)prompt",
    r"(act|pretend|roleplay) as",
    r"DAN",
    r"jailbreak",
    r"override",
]
👁️
PII Detection
High

Scans both prompts and completions for personally identifiable information. Evidence is automatically redacted before storage (e.g., SSN → ***-**-6234).

analyzers/security.py
PII_PATTERNS = {
    "ssn":         r"\b\d{3}-\d{2}-\d{4}\b",
    "credit_card": r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b",
    "email":       r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
    "phone":       r"\b(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b",
}
🔑
Secret / API Key Leakage
Critical

Catches API keys and credentials that have accidentally made it into a prompt or completion. Covers major providers and generic patterns.

analyzers/security.py
SECRET_PATTERNS = {
    "openai_key":    r"sk-[A-Za-z0-9]{32,}",
    "anthropic_key": r"sk-ant-[A-Za-z0-9\-]{32,}",
    "aws_key":       r"AKIA[0-9A-Z]{16}",
    "github_token":  r"ghp_[A-Za-z0-9]{36}",
    "generic_key":   r"(api[_-]?key|secret|token)\s*[:=]\s*['\"]?[A-Za-z0-9\-_.]{16,}",
}
⚠️
Behavioral Anomaly
Medium

Detects high-risk tool call sequences within a trace. These patterns may indicate a jailbreak attempt or a compromised system prompt.

analyzers/security.py
RISKY_TOOL_SEQUENCES = [
    ["read_file", "send_email"],            # exfiltration
    ["read_file", "write_file", "delete"], # read-modify-destroy
    ["search_web", "execute_code"],        # search-then-exec
]

Security Score

Each trace receives a Security Score from 0–100, calculated after the trace completes. A score of 100 means no alerts were triggered.

analyzers/security.py
def calculate_security_score(alerts):
    score = 100
    deductions = {
        "critical": 40,
        "high":     20,
        "medium":   10,
        "low":       5,
    }
    for alert in alerts:
        score -= deductions[alert.severity]
    return max(0, score)
ScoreMeaning
80 – 100Clean — no or only low-severity alerts
60 – 79Needs attention — at least one medium/high alert
0 – 59Critical — prompt injection or secret leakage detected

API Reference — Traces

POST/api/v1/traces

Create a new trace. Returns the trace ID used to associate spans.

PATCH/api/v1/traces/{id}

Update a trace (e.g., set status to completed or failed when the agent finishes).

GET/api/v1/traces

List all traces. Supports pagination and status filtering.

GET/api/v1/traces/{id}

Get full trace details including all spans, insights, and security score.

GET/api/v1/traces/{id}/spans

Get the span tree for a trace.

GET/api/v1/traces/{id}/insights

Get all Observation insights (cost suggestions) for a trace.

GET/api/v1/traces/{id}/security

Get the full security report for a trace — alerts, score, and per-span flags.

API Reference — Spans

POST/api/v1/spans

Record a single span. Used by the SDK and proxy.

POST/api/v1/spans/batch

Record multiple spans in one request. Preferred for high-throughput agents.

Span fields

FieldTypeRequiredDescription
trace_idstringParent trace ID
namestringStep name (e.g., "call_gpt4o")
span_typestringllm / tool / chain / agent
modelstringModel ID (e.g., "gpt-4o")
prompt_tokensintInput token count
completion_tokensintOutput token count
cost_usdfloatCost in USD (auto-calculated if omitted)
prompt_previewstringFirst 500 chars of prompt — scanned by Security
completion_previewstringFirst 500 chars of completion — scanned by Security
tool_namestringFor tool spans
tool_inputJSONTool input — used for repeated-call detection
started_atdatetimeISO 8601
ended_atdatetimeISO 8601

API Reference — Security

GET/api/v1/security/alerts

List all security alerts. Filter by severity, status, alert_type, or time range.

GET/api/v1/security/alerts/{id}

Get a single alert with full evidence (redacted) and linked trace/span.

PATCH/api/v1/security/alerts/{id}/status

Update alert status. Body: {"status": "acknowledged"} or {"status": "resolved"}.

GET/api/v1/security/stats

Summary counts by severity and type. Used by the Security Alert Center overview cards.

API Reference — Stats

GET/api/v1/stats/overview

Returns aggregate totals for the dashboard Overview page: total cost, trace count, wasted cost, flagged trace count, and security metrics.

GET/api/v1/stats/cost-by-model

Cost breakdown by model. Used by the Analytics page pie/bar chart.

GET/api/v1/stats/cost-by-trace

Top traces by total cost. Used by the "Most Expensive Traces" table in Analytics.


Data Model

Jasmind uses SQLite in MVP (PostgreSQL-compatible schema). Four tables:

traces

SQL
CREATE TABLE traces (
    id              TEXT PRIMARY KEY,
    name            TEXT,
    status          TEXT,   -- running / completed / failed / loop_detected
    started_at      DATETIME,
    ended_at        DATETIME,
    total_tokens    INTEGER,
    total_cost_usd  REAL,
    security_score  INTEGER, -- 0–100
    metadata        JSON
);

spans

SQL
CREATE TABLE spans (
    id                   TEXT PRIMARY KEY,
    trace_id             TEXT REFERENCES traces(id),
    parent_id            TEXT,
    name                 TEXT,
    span_type            TEXT,   -- llm / tool / chain / agent
    model                TEXT,
    started_at           DATETIME,
    ended_at             DATETIME,
    latency_ms           INTEGER,
    prompt_tokens        INTEGER,
    completion_tokens    INTEGER,
    cost_usd             REAL,
    prompt_preview       TEXT,   -- first 500 chars
    completion_preview   TEXT,
    tool_name            TEXT,
    tool_input           JSON,
    tool_output          TEXT,
    obs_flags            JSON,   -- ["loop", "long_context", "repeated_call"]
    sec_flags            JSON    -- ["prompt_injection", "pii_leak", "secret_leak", ...]
);

security_alerts

SQL
CREATE TABLE security_alerts (
    id          TEXT PRIMARY KEY,
    trace_id    TEXT REFERENCES traces(id),
    span_id     TEXT REFERENCES spans(id),
    created_at  DATETIME,
    severity    TEXT,   -- critical / high / medium / low
    alert_type  TEXT,   -- prompt_injection / pii_leak / secret_leak / behavioral_anomaly
    description TEXT,
    evidence    TEXT,   -- redacted
    status      TEXT DEFAULT 'open' -- open / acknowledged / resolved
);

insights

SQL
CREATE TABLE insights (
    id                   TEXT PRIMARY KEY,
    trace_id             TEXT REFERENCES traces(id),
    created_at           DATETIME,
    wasted_cost_usd      REAL,
    waste_reason         TEXT,   -- loop_detected / oversized_context / repeated_calls
    suggestion_type      TEXT,   -- cheaper_model / reduce_context / add_caching
    suggestion_detail    TEXT,
    estimated_saving_usd REAL,
    quality_impact       TEXT,   -- minimal / moderate / significant
    risk_change          TEXT    -- lower / same / higher
);