Context Tagging
Attach metadata -- like a call type, tags, or environment -- to every span created inside a code block, without touching each span individually.
Why Context Tagging?
A typical agent makes many LLM calls: one to plan, one to act, one to judge quality. If every call is auto-instrumented, you need a way to tell Insight which call is which. tracing_context() lets you wrap a section of code so that all spans inside it carry the same labels.
Tag All Spans in a Code Block
tracing_context() is a context manager. Any span created inside the with block automatically receives the metadata you pass in.
import mantisdk.tracing as tracing
tracing.init()
with tracing.tracing_context(call_type="agent-call", tags=["planning", "v2"]):
# Every LLM call inside this block is tagged as "agent-call"
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Plan the next step."}],
)In Insight, the spans from that block will show call_type = agent-call and tags = ["planning", "v2"], making them easy to filter and compare.
Parameters
| Parameter | Type | Description |
|---|---|---|
call_type | str | Label for the kind of LLM call -- e.g. "agent-call", "judge-call", "reflection-call" |
tags | list[str] | Freeform tags attached to every span in the block |
environment | str | Environment name for filtering -- e.g. "production", "staging" |
session_id | str | Session identifier for grouping related spans |
All parameters are optional and keyword-only. Only the values you provide are set; the rest remain unchanged.
Create Reusable Decorators with call_type_decorator
If you always tag certain functions the same way, call_type_decorator() creates a decorator you can reuse across your codebase.
import mantisdk.tracing as tracing
tracing.init()
# Define once at module level
agent = tracing.call_type_decorator("agent-call")
judge = tracing.call_type_decorator("judge-call")
@agent
def run_agent(query: str) -> str:
# All LLM spans inside are tagged as "agent-call"
return client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": query}],
).choices[0].message.content
@judge
def evaluate_response(response: str) -> float:
# All LLM spans inside are tagged as "judge-call"
result = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Rate this response: {response}"}],
)
return float(result.choices[0].message.content)The decorator works with both sync and async functions -- no special handling needed.
@judge
async def evaluate_response_async(response: str) -> float:
result = await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Rate this response: {response}"}],
)
return float(result.choices[0].message.content)Read the Current Context
Three getter functions let you inspect what context is active. This is useful for logging, assertions, or conditional logic inside shared utilities.
import mantisdk.tracing as tracing
with tracing.tracing_context(call_type="agent-call", tags=["experiment-42"]):
print(tracing.get_current_call_type()) # "agent-call"
print(tracing.get_current_tags()) # ["experiment-42"]
print(tracing.get_current_environment()) # None (not set in this block)Outside any tracing_context block, all three return None.
| Function | Returns |
|---|---|
get_current_call_type() | str or None |
get_current_tags() | list[str] or None |
get_current_environment() | str or None |
Nest Contexts
Contexts nest cleanly. An inner block overrides the outer block's values for its duration; when it exits, the outer values are restored.
import mantisdk.tracing as tracing
with tracing.tracing_context(call_type="agent-call", tags=["outer"]):
# Spans here: call_type="agent-call", tags=["outer"]
with tracing.tracing_context(call_type="judge-call", tags=["inner"]):
# Spans here: call_type="judge-call", tags=["inner"]
pass
# Back to: call_type="agent-call", tags=["outer"]You can also override only some fields. Unspecified fields keep the value from the outer context.
with tracing.tracing_context(call_type="agent-call", tags=["experiment"]):
# call_type="agent-call", tags=["experiment"]
with tracing.tracing_context(call_type="judge-call"):
# call_type="judge-call", tags still=["experiment"]
passEach tracing_context only touches the fields you pass in. If you set call_type but not tags, the surrounding tags remain active inside the block.
Async and Thread Safety
Context tagging uses Python contextvars, which means:
- Async: Each
asynciotask gets its own copy of the context. Tagging in one coroutine does not leak into another. - Threads: Each thread has its own context by default. Tagging in one thread does not affect another.
You can safely use tracing_context() and call_type_decorator() in concurrent code without locking.
import asyncio
import mantisdk.tracing as tracing
async def run_agent(query: str):
with tracing.tracing_context(call_type="agent-call"):
return await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": query}],
)
async def run_judge(response: str):
with tracing.tracing_context(call_type="judge-call"):
return await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Rate: {response}"}],
)
# These run concurrently -- each task keeps its own context
agent_result, judge_result = await asyncio.gather(
run_agent("Plan the next step."),
run_judge("The agent completed the task."),
)Session Tracking
Sessions group related traces together in Insight. A chatbot conversation, a multi-step workflow, or a series of agent interactions that belong to the same logical unit can all share a session ID. In the Insight UI, you can filter and browse traces by session to see the full picture.
Tag All Traces in a Session
Pass session_id to tracing_context() and every trace created inside the block is linked to that session.
import mantisdk.tracing as tracing
tracing.init()
session_id = "chat-session-abc123"
with tracing.tracing_context(session_id=session_id):
# Each trace in this block belongs to the same session
with tracing.trace("user-message-1", input="What is Mantis?") as span:
response = process("What is Mantis?")
span.set_attribute(tracing.semconv.TRACE_OUTPUT, response)
with tracing.trace("user-message-2", input="How do I install it?") as span:
response = process("How do I install it?")
span.set_attribute(tracing.semconv.TRACE_OUTPUT, response)Both traces appear under session chat-session-abc123 in Insight.
Set Session on Individual Spans
For more granular control, set SESSION_ID directly on a span:
with tracing.trace("workflow", input=query) as span:
span.set_attribute(tracing.semconv.SESSION_ID, "session-456")
span.set_attribute(tracing.semconv.USER_ID, "user-123")
result = do_work(query)
span.set_attribute(tracing.semconv.TRACE_OUTPUT, result)Combine Sessions with Other Context
session_id works alongside call_type, tags, and environment. Mix them freely:
with tracing.tracing_context(
session_id="onboarding-session-001",
environment="production",
tags=["onboarding", "v2"],
):
with tracing.tracing_context(call_type="agent-call"):
response = agent.run("Help me set up my account")
with tracing.tracing_context(call_type="judge-call"):
score = judge.evaluate(response)Chatbot Example
A typical pattern for a chatbot that maintains session context across multiple user messages:
import uuid
import mantisdk.tracing as tracing
tracing.init()
def handle_conversation(user_id: str):
session_id = f"chat-{uuid.uuid4().hex[:12]}"
with tracing.tracing_context(session_id=session_id):
while True:
user_input = input("> ")
if user_input in ("quit", "exit"):
break
with tracing.trace("chat-turn", input=user_input) as span:
span.set_attribute(tracing.semconv.USER_ID, user_id)
response = agent.respond(user_input)
span.set_attribute(tracing.semconv.TRACE_OUTPUT, response)
print(response)
tracing.shutdown()Every turn in the conversation is a separate trace, but they all share the same session ID. In Insight, open the session to see the full conversation flow.
Session IDs can be any string. UUIDs, human-readable slugs like "onboarding-session-001", or composite keys like "user-123:chat-456" all work. Pick a format that makes sessions easy to find in the Insight UI.
When running inside a Mantis Run (experiment), sessions are tracked automatically -- you don't need to set session_id yourself.
Complete Example
Putting it all together -- an agent loop that tags planning, execution, and evaluation separately.
import mantisdk.tracing as tracing
tracing.init(service_name="my-agent")
agent = tracing.call_type_decorator("agent-call")
judge = tracing.call_type_decorator("judge-call")
@agent
def plan(goal: str) -> str:
return client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Plan how to: {goal}"}],
).choices[0].message.content
@agent
def execute(plan: str) -> str:
return client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Execute this plan: {plan}"}],
).choices[0].message.content
@judge
def score(result: str) -> float:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Score 0-1: {result}"}],
)
return float(response.choices[0].message.content)
def run(goal: str):
with tracing.tracing_context(tags=["experiment-42"], environment="staging"):
with tracing.trace("agent-loop", input=goal):
the_plan = plan(goal)
result = execute(the_plan)
quality = score(result)
return {"result": result, "score": quality}
run("Summarize today's news")
tracing.shutdown()In Insight you can now filter by call_type to see agent calls vs judge calls, by tags to find a specific experiment, and by environment to separate staging from production.
Next Steps
- Traces & Spans -- Create and nest spans manually
- Semantic Conventions -- Standard attribute names
- Async Support -- Async-specific span creation