Documentation
Simple guide: connect Cartha, run an agent, see traces, memory, costs, and guardrails in the dashboard.
1. Quick Start
Start HereDo this once. Then run any agent code you already have. Cartha records the run so you can open the dashboard and see what happened.
Step A — Install & key
pip install cartha-sdk # From https://cartha.in → Keys / Settings export CARTHA_API_KEY="cartha_..." export CARTHA_API_BASE="https://cartha.in"
Step B — Two lines at app startup
import cartha cartha.init(api_key="cartha_...", api_base="https://cartha.in") cartha.instrument() # Your normal OpenAI / framework code runs below from openai import OpenAI client = OpenAI() client.chat.completions.create({ model="gpt-4o-mini", messages=[{"role": "user", "content": "Hello"}], })
@cartha.trace + @cartha.tool. That gives you a named agent, budgets, and tool allow-lists. instrument() is the zero-code shortcut for supported LLM clients.What you see in the dashboard
Step C — Recommended full example (named agent)
This is the pattern most teams use in production. Easy to read and fully visible in the UI.
import os
import cartha
cartha.init(
api_key=os.environ["CARTHA_API_KEY"],
api_base="https://cartha.in",
)
client = cartha.wrap_openai() # auto LLM + cost steps
@cartha.tool()
def crm_lookup(user_id: str) -> dict:
return {"plan": "pro"}
@cartha.trace(
id="support_agent",
team="support",
budget_usd=0.50,
allowed_tools=["crm_lookup"],
)
async def handle_ticket(user_id: str, ticket: str) -> str:
await cartha.remember(
user_id=user_id,
content="Ticket: " + ticket,
scope="user",
)
hits = await cartha.recall(
user_id=user_id,
context=ticket,
scope=["user", "team"],
top_k=5,
)
data = crm_lookup(user_id)
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": ticket + "\nCRM: " + str(data) + "\nMemory: " + str(hits),
}],
)
return r.choices[0].message.content or ""What you see in the dashboard
support_agent, a full trace with tool + memory + LLM steps, and costs under the budget of $0.50.2. Supported Frameworks
Use whatever stack you already have. Cartha sits beside it. Depth of automatic patching varies by framework—OpenAI and LangGraph are the most mature today. Everything else works with the Core SDK decorators.
@cartha.trace and your tools with @cartha.tool. Same dashboard.OpenAI
LangGraph
CrewAI
Custom Python
Anthropic / Gemini / LiteLLM
AutoGen / PydanticAI / LlamaIndex
Example — OpenAI only
import cartha
from openai import OpenAI
cartha.init(api_key="cartha_...", api_base="https://cartha.in")
cartha.instrument(["openai"]) # only patch OpenAI
client = OpenAI()
client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Summarize today's tickets"}],
)3. Automatic Features
Zero-CodeWhen you call cartha.instrument(), Cartha tries to monkey-patch installed libraries so normal API calls become traces. Think of it as “turn on recording for this process.”
Discovery
- Detect installed SDKs (e.g. OpenAI, LangGraph) and patch what is available.
- Print a short status banner so you know what got instrumented.
Governance (with Core SDK)
Hard budgets, tool allow-lists, and policies work best with explicit @cartha.trace(...).
@cartha.trace(id="research", team="ops", budget_usd=0.25)
async def research(question: str):
# If LLM spend crosses $0.25 → BudgetExceeded, run stops
...Observability
- LLM calls become cost + model steps on a trace (estimated USD from tokens).
- Tool calls show inputs/outputs when decorated with
@cartha.tool— or when LangGraphBaseToolis patched (allow-list checked before the tool body). - Failed runs finish as
failurewith real duration so Traces stay accurate.
4. CLI Reference
After pip install cartha-sdk, you get a cartha command for health checks.
cartha doctor
Checks Python, CARTHA_API_KEY, connectivity to /health, and which frameworks are installed.
$ cartha doctor ✓ CARTHA_API_KEY is set ✓ Backend Connectivity OK ✓ Frameworks Detected: openai, langgraph, ...
cartha instrument
Run a script with auto-instrument env set (handy for demos).
cartha instrument python app.pycartha config
Prints whether CARTHA_API_KEY / base are set (does not print the full secret).
cartha version
Shows installed SDK version (e.g. 0.4.4).
5. Core SDK
Use TheseThree ideas cover almost everything: connect, auto-patch, or watch one graph.
cartha.init(...)
Connects the SDK to your workspace. Call once at process start.
cartha.init(
api_key="cartha_...", # or env CARTHA_API_KEY
api_base="https://cartha.in", # or env CARTHA_API_BASE
)cartha.instrument()
Patches supported installed libraries (e.g. OpenAI chat completions, LangGraph runs). Optional list: instrument(["openai"]).
cartha.instrument() # all available cartha.instrument(["openai"]) # only OpenAI
cartha.observe(graph)
Prefer this when you only want one LangGraph app instrumented.
app = graph.compile()
app = cartha.observe(app)
app.invoke({"messages": [...]})What you see in the dashboard
init + a real API call, new rows appear under Traces. If nothing shows up, check the API key and that api_base is https://cartha.in.6. Advanced Instrumentation
Manual APIsWhen to use this section
Custom agents, non-OpenAI models, hard budgets, tool allow-lists, or memory isolation. These APIs are the most reliable path for production governance.
cartha.wrap_openai()
Wrap one OpenAI client so every chat.completions.create becomes an LLM step with cost.
from openai import OpenAI
import cartha
cartha.init(api_key="cartha_...", api_base="https://cartha.in")
client = cartha.wrap_openai(OpenAI()) # or wrap_openai() and it creates a client
# Normal OpenAI code — now traced
client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hello"}],
)@cartha.trace(...)
Marks one agent run: registers the agent, starts/finishes a trace, optional budget and allowed_tools.
@cartha.trace(
id="support_agent",
team="support",
budget_usd=0.50,
allowed_tools=["crm_lookup"],
)
async def handle_ticket(user_id: str, ticket: str) -> str:
...
return "done"What you see in the dashboard
support_agent. Each call creates a Trace. Over budget → failure with BudgetExceeded.@cartha.tool()
Records tool inputs/outputs. Prefer async tools inside async agents. Tools not listed in allowed_tools raise ToolNotAuthorized before the function body runs.
@cartha.tool()
async def crm_lookup(user_id: str) -> dict:
return {"user_id": user_id, "plan": "pro"}
@cartha.tool()
async def initiate_wire(amount: float) -> dict:
return {"status": "sent"} # dangerous — keep off allow-list for support
@cartha.trace(id="support", allowed_tools=["crm_lookup"])
async def run(user_id: str):
await crm_lookup(user_id) # OK
# await initiate_wire(100) # ToolNotAuthorizedremember / recall (scoped memory)
Scopes: user, agent, team, org. Always pass the real user_id so customers stay isolated.
# Store a fact about this customer only
await cartha.remember(
user_id="alice_42",
content="Prefers email follow-ups",
scope="user",
confidence=0.9,
)
# Search only allowed scopes
hits = await cartha.recall(
user_id="alice_42",
context="how should I contact them?",
scope=["user", "team"],
top_k=5,
)
# Bob using user_id="bob_99" will not see Alice's user-scoped note
# Time-travel: what did the agent know then?
hits_then = await cartha.recall(
user_id="alice_42",
context="refund policy",
scope=["user"],
as_of="2026-07-12T14:30:00Z",
)What you see in the dashboard
as_of to replay what the agent knew at that moment.llm_call (any model)
Not using OpenAI? Call your model yourself, then record the step so costs and traces still work. Auto-instrumented Anthropic / Gemini / LiteLLM also estimate cost from tokens so budgets can trip.
# reply = your_gemini_or_local_model(prompt)
await cartha.llm_call(
model="gemini-flash",
input=prompt,
output=reply,
tokens_in=120,
tokens_out=80,
total_cost_usd=0.0002,
)Handle budget stop
from cartha import BudgetExceeded
try:
await handle_ticket("user_1", "refund please")
except BudgetExceeded as e:
print("Stopped by Cartha:", e)
# notify user / fallback agent — or use run_with_failover()7. Memory, failover & more
Features that complete a production fleet: time-travel memory, agent-task fit, failover chains, and no-code ingest. Full detail also lives on PyPI (cartha-sdk README).
Time-travel recall
Answer "why did it say that last Tuesday?" — as_of returns memory as it stood then (erased memories still never resurface).
hits = await cartha.recall(
user_id="u1",
context="refund policy",
scope=["user"],
as_of="2026-07-12T14:30:00Z",
)Agent-task fit & failover
Declare task_type so agents become comparable. When one hits its budget ceiling, run_with_failover hands the same task_id to the next agent.
@cartha.trace(id="premium", team="support",
task_type="invoice_refund", budget_usd=5.0)
async def premium(user_id: str, invoice_id: str): ...
@cartha.trace(id="backup", team="support",
task_type="invoice_refund", budget_usd=1.0)
async def backup(user_id: str, invoice_id: str): ...
result = await cartha.run_with_failover(
task_type="invoice_refund",
handlers={"premium": premium, "backup": backup},
user_id="customer_123",
invoice_id="8401",
)What you see in the dashboard
Attenuated delegate
Parent grants a child a subset of remaining budget and tools — a child can never escalate beyond the parent's grant.
grant = await cartha.delegate(
to_agent_id="worker",
task_description="sub-task",
budget_usd=2.00,
)
# pass grant budget fields into the child @trace run, then close_budgetNo-code ingest (n8n / Make / …)
One HTTP POST at the end of a workflow — same traces, costs, and task-fit as Python.
curl -X POST https://cartha.in/api/v1/ingest/run \
-H "X-Api-Key: $CARTHA_API_KEY" -H "Content-Type: application/json" \
-d '{"agent":"support_workflow","platform":"n8n","status":"success","steps":[]}'8. Universal Runtime Model
Different frameworks have different APIs. Cartha maps them into one simple story so the dashboard always looks the same: a run, with steps (tool, LLM, memory, cost, error).
Simple map of what you see
Trace started / finishedOne agent run from start to end.
Tool stepA function decorated with @cartha.tool ran.
LLM stepA model call (wrap_openai or llm_call).
Memory store / recallremember() / recall() calls.
CostSpend that feeds budget breakers.
Error / BudgetExceededRun stopped or step failed.
9. Architecture
Cartha is designed to stay out of your business logic. You add a few lines; your agent still calls the same models and tools.
- Your process — agent code + cartha-sdk
- Cartha API — stores traces, memory, costs; enforces policies and plan limits
- Dashboard — same login as keys; for humans to inspect runs
- Fail-open telemetry — if recording fails, prefer not to crash your agent (governance errors like budgets/policies still raise on purpose)
Agent app --SDK--> cartha.in API --> Postgres / Redis / vectors
\
--> Dashboard (you look here)10. Capability Matrix
What auto-instrumentation actually patches today. Use @cartha.tool / remember / wrap_openai when a cell is empty.
LLM providers
| Provider | Auto instrument | Manual llm_call | Costs → budgets |
|---|---|---|---|
| OpenAI | ✅ wrap / instrument | ✅ | ✅ estimated |
| Anthropic | ✅ instrument | ✅ | ✅ estimated |
| Gemini / LiteLLM / LlamaIndex | ✅ instrument | ✅ | ✅ estimated |
Agent frameworks (auto depth)
| Framework | Tools gate | Execution graph | Streaming |
|---|---|---|---|
| LangGraph | ✅ BaseTool | ✅ | ✅ |
| CrewAI / Pydantic AI / AutoGen / ADK | use @tool | ✅ spans | — |
Governance (all stacks)
| Feature | How | Dashboard |
|---|---|---|
| Traces | @trace / instrument | Traces |
| Tools | @tool + allowed_tools (pre-body gate) | Trace steps |
| Memory isolation | remember / recall scopes (+ as_of) | Memory |
| Hard budgets | budget_usd on @trace | Costs + failed run |
| Failover / task-fit | task_type + run_with_failover | Intelligence |
| Policies | Dashboard policies | Policies |
11. Examples
Copy, set your key, run, then open Traces.
A. Minimal (no OpenAI)
import asyncio, os, cartha
cartha.init(
api_key=os.environ["CARTHA_API_KEY"],
api_base="https://cartha.in",
)
@cartha.tool()
def lookup_order(order_id: str) -> dict:
return {"order_id": order_id, "status": "shipped"}
@cartha.trace(id="support_agent", team="support", budget_usd=1.0)
async def handle(user_id: str, question: str) -> str:
order = lookup_order("ORD-1")
reply = f"Order {order['order_id']} is {order['status']}."
await cartha.llm_call(
model="mock",
input=question,
output=reply,
tokens_in=10,
tokens_out=20,
total_cost_usd=0.0001,
)
return reply
if __name__ == "__main__":
print(asyncio.run(handle("user_1", "Where is my order?")))
print("→ https://cartha.in → Traces")B. OpenAI + memory
import os, cartha
cartha.init(api_key=os.environ["CARTHA_API_KEY"], api_base="https://cartha.in")
client = cartha.wrap_openai()
@cartha.trace(id="support_agent", team="support", budget_usd=0.5)
async def handle(user_id: str, ticket: str) -> str:
await cartha.remember(user_id=user_id, content=ticket, scope="user")
hits = await cartha.recall(
user_id=user_id, context=ticket, scope=["user"], top_k=3
)
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{ticket}\nMemory: {hits}"}],
)
return r.choices[0].message.content or ""C. LangGraph (auto)
import cartha
from langgraph.graph import StateGraph
cartha.init(api_key="cartha_...", api_base="https://cartha.in")
cartha.instrument()
# ... build StateGraph ...
app = graph.compile()
# optional: app = cartha.observe(app)
app.invoke({"input": "hello"}) # run appears under TracesD. CrewAI-style entry
import cartha
# from crewai import Crew, Agent, Task
cartha.init(api_key="cartha_...", api_base="https://cartha.in")
@cartha.trace(id="research_crew", team="research", budget_usd=1.0)
def run_crew(topic: str):
# crew = Crew(agents=[...], tasks=[...])
# return crew.kickoff(inputs={"topic": topic})
return f"researched {topic}"
run_crew("agent governance")@cartha.trace so the whole crew run is one parent trace you can open in the UI.12. Migration Guide
Moving from manual wrap-only style to optional auto-instrumentation (v0.4+).
Still valid (v0.3 style)
client = OpenAI() client = cartha.wrap_openai(client)
Also fine (v0.4)
cartha.instrument() client = OpenAI() # patched if OpenAI installed
@trace, @tool, and wrap_openai. They remain the recommended path for budgets and allow-lists.13. FAQ
Do I still need @cartha.trace()?▼
For production governance—yes, recommended. instrument() can record LLM calls automatically, but @trace gives a clear agent name, team, budget_usd, and allowed_tools.
Where do I put the API key?▼
Environment variable CARTHA_API_KEY (and CARTHA_API_BASE=https://cartha.in). Get the key after login at cartha.in → Keys / Settings. Never commit keys to git.
Nothing shows in the dashboard▼
Check: (1) key is correct, (2) base is https://cartha.in not api.cartha.in, (3) you actually called a traced function or instrumented LLM, (4) refresh Traces and filter by agent id.
What is HTTP 402?▼
Plan limit (e.g. agent count or monthly steps). Delete unused agents in the dashboard or upgrade plan on the pricing page.
What is ToolNotAuthorized?▼
The tool name is not in allowed_tools on @cartha.trace. The gate runs before the tool body (@cartha.tool and LangGraph BaseTool). Add it to the list only if that agent should run it.
What is BudgetExceeded?▼
Spend on that run went over budget_usd. Catch it and fail gracefully, or hand off with run_with_failover().
Can Customer A see Customer B memory?▼
Not if you use scope="user" and different user_id values. Isolation is enforced server-side. Never reuse one user_id for all customers.
How do I replay what an agent knew last week?▼
Pass as_of=... (ISO timestamp or datetime) to recall(). Erased memories never resurface, even in time travel.
Does Cartha replace LangGraph / CrewAI?▼
No. Keep your framework. Cartha is the control plane: traces, memory scopes, budgets, tool authority, dashboard.
What if Cartha is down?▼
Telemetry is designed not to take down your app when possible. Budget and policy denials still raise—they are safety controls, not optional logs.