Jasmind Documentation
The observation and security layer for production AI agents. Instrument your agent in minutes — no rewrites, no external dependencies.
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 |
Quick Start
Get observation and security running in your agent in under 5 minutes.
-
1Start the Jasmind serverClone the repo and run Docker Compose. The collector API starts on
:8080, the dashboard on:3000. -
2Install the SDKOne package with no required dependencies beyond your existing agent stack.
-
3Wrap your agentAdd three lines. Both Observation and Security activate automatically.
-
4Open the dashboardGo to
http://localhost:3000— traces and security alerts appear in real time as your agent runs.
# 1. Start the server git clone https://github.com/jasmind-ai/jasmind docker-compose up -d # 2. Install the SDK pip install jasmind
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
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
docker-compose up -d # Collector API → http://localhost:8080 # Dashboard → http://localhost:3000
| Service | Port | Description |
|---|---|---|
| Backend (FastAPI) | 8080 | Trace collector API + analyzers |
| Frontend (React) | 3000 | Dashboard UI |
| Database (SQLite) | — | Stored at ./data/jasmind.db, survives container restarts |
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.
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.
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}], )
| Parameter | Type | Description |
|---|---|---|
endpoint | str | URL of your Jasmind collector. Default: http://localhost:8080 |
name | str | Human-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.
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.
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.
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.
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.
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.
# 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 Type | Trigger | Example |
|---|---|---|
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.
Matches instruction-override patterns in any span's prompt text. Fires when a user-provided input attempts to redirect the agent's behavior.
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", ]
Scans both prompts and completions for personally identifiable information. Evidence is automatically redacted before storage (e.g., SSN → ***-**-6234).
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", }
Catches API keys and credentials that have accidentally made it into a prompt or completion. Covers major providers and generic patterns.
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,}", }
Detects high-risk tool call sequences within a trace. These patterns may indicate a jailbreak attempt or a compromised system prompt.
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.
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)
| Score | Meaning |
|---|---|
| 80 – 100 | Clean — no or only low-severity alerts |
| 60 – 79 | Needs attention — at least one medium/high alert |
| 0 – 59 | Critical — prompt injection or secret leakage detected |
API Reference — Traces
Create a new trace. Returns the trace ID used to associate spans.
Update a trace (e.g., set status to completed or failed when the agent finishes).
List all traces. Supports pagination and status filtering.
Get full trace details including all spans, insights, and security score.
Get the span tree for a trace.
Get all Observation insights (cost suggestions) for a trace.
Get the full security report for a trace — alerts, score, and per-span flags.
API Reference — Spans
Record a single span. Used by the SDK and proxy.
Record multiple spans in one request. Preferred for high-throughput agents.
Span fields
| Field | Type | Required | Description |
|---|---|---|---|
trace_id | string | ✓ | Parent trace ID |
name | string | ✓ | Step name (e.g., "call_gpt4o") |
span_type | string | ✓ | llm / tool / chain / agent |
model | string | Model ID (e.g., "gpt-4o") | |
prompt_tokens | int | Input token count | |
completion_tokens | int | Output token count | |
cost_usd | float | Cost in USD (auto-calculated if omitted) | |
prompt_preview | string | First 500 chars of prompt — scanned by Security | |
completion_preview | string | First 500 chars of completion — scanned by Security | |
tool_name | string | For tool spans | |
tool_input | JSON | Tool input — used for repeated-call detection | |
started_at | datetime | ✓ | ISO 8601 |
ended_at | datetime | ✓ | ISO 8601 |
API Reference — Security
List all security alerts. Filter by severity, status, alert_type, or time range.
Get a single alert with full evidence (redacted) and linked trace/span.
Update alert status. Body: {"status": "acknowledged"} or {"status": "resolved"}.
Summary counts by severity and type. Used by the Security Alert Center overview cards.
API Reference — Stats
Returns aggregate totals for the dashboard Overview page: total cost, trace count, wasted cost, flagged trace count, and security metrics.
Cost breakdown by model. Used by the Analytics page pie/bar chart.
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
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
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
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
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 );