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

SituationUse
A function is the unit of work@capsera.agent()
Narrower than a function, or runtime-determinedtag()
One label on everything downstream in a requestset_session()
A graph node or crew agentlanggraph_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.