Tags and sessions
tag() for attribution narrower than a function, and set_session() for grouping every call in a request or conversation.
tag()
tag() is a context manager taking the same fields as the decorator. Use it when the
unit of work is decided at runtime, or is narrower than the function containing it:
from capsera import tag
def handle(request):
with tag("classifier", task_type="triage"):
label = classify(request.text)
with tag("responder", task_type="generation", customer_id=request.customer_id):
return respond(label, request.text)
One function, two agents, and a customer_id only available at runtime.
It nests with decorators and with itself, following the same per-field resolution:
@capsera.agent("batch-runner", team="ops")
def run(customers):
for customer in customers:
with tag("enricher", customer_id=customer.id):
enrich(customer) # agent=enricher, team=ops, customer_id per row
That loop is the main use for tag(). It turns a single total for enrichment into
per-customer figures.
set_session()
A session is a value attached to the current context that applies to every subsequent call until cleared. Use it for an identifier you learn once and want on everything downstream.
capsera.set_session(request_id)
Middleware is the usual place, because that is where the request ID is available and you do not know which code path will make a model call:
@app.middleware("http")
async def attribute_session(request, call_next):
capsera.set_session(request.headers.get("x-request-id") or str(uuid4()))
try:
return await call_next(request)
finally:
capsera.clear_session()
Every call in that request now shares a session_id, with no decorators in the
handlers.
Always clear the session. Web servers reuse threads and tasks, so a session left set
can carry into the next request handled by the same worker and mislabel it. The
finally block is required for correctness, not defensive style.
For a multi-turn conversation, use the conversation ID rather than the request ID, so the session groups the whole exchange.
To set a session for one scope only, pass it to tag():
with tag("chat", session_id=conversation.id):
...
Choosing an API
| Situation | Use |
|---|---|
| A function is the unit of work | @capsera.agent() |
| Narrower than a function, or runtime-determined | tag() |
| One label on everything downstream in a request | set_session() |
| A graph node or crew agent | langgraph_node() or crewai_agent() |
Concurrency
All three are contextvars-based, so a tag() in one thread or task is invisible to
every other. Ten coroutines under asyncio.gather, each with its own tag(), produce
ten separate attributions.
A task created outside a tagged scope does not inherit it, because there was nothing to inherit when the task was created. Apply the tag inside the task if you need it there.