Attribution model

The six fields every event carries, how nested scopes resolve, and why attribution stays isolated across threads and asyncio tasks.

Fields

FieldSet byAnswers
agent@agent(), tag(), or agent_name= in init()Which logical unit spent this
teamthe sameWhich team owns the spend
task_typethe sameWhat kind of work it was
session_idset_session() or tag()Which conversation or request it belonged to
customer_id@agent() or tag()Which of your customers to bill
cost_centerthe sameWhich finance budget line

Every event also carries the model, provider, token counts, cost, latency, environment, and the file and line that made the call. You do not supply any of these.

agent has a fallback. A call made with no attribution in scope is recorded as unknown. It is never dropped, because an unattributed cost is still a cost.

Nested scopes resolve per field

@capsera.agent("pipeline", team="research")
def run(question):
    plan = make_plan(question)          # agent=pipeline, team=research
    return summarize(plan)


@capsera.agent("summarizer")             # no team set
def summarize(text):
    ...                                  # agent=summarizer, team=research

summarize overrides agent but sets no team, so team still comes from the enclosing scope. Fields resolve one at a time, so an inner decorator can refine attribution without restating what its caller established.

Isolation under concurrency

The context stack uses contextvars, not threading.local.

Threads. Each thread has its own stack. Eight workers in a ThreadPoolExecutor with different attribution produce eight correctly attributed streams.

asyncio tasks. contextvars propagate into tasks at creation and stay isolated per task, so asyncio.gather over ten differently tagged coroutines keeps their attribution separate. threading.local would not, because every task on one event loop shares a thread.

The failure this prevents is one agent's spend appearing on another agent's line. The SDK's test suite asserts zero context bleed under both models, and the evaluation harness treats any bleed as a run failure.

Choosing between the three APIs

Use a decorator when the unit of work is a function. This covers most cases, including graph nodes and agent classes.

Use a context manager when the unit is narrower than a function or is determined at runtime, such as one branch of a handler or one iteration of a per-customer loop:

with capsera.tag("enricher", customer_id=customer.id):
    ...

Use a session for a label that should apply to everything downstream until cleared, such as a request ID set in middleware:

capsera.set_session(request_id)

See Agents and Tags and sessions for details.

Limits

Attribution does not cross a process boundary. If your agents run as separate services, each process needs its own init() and its own attribution. Nothing propagates over HTTP automatically. To correlate spend across services, pass a session_id between them yourself.