Agents

The @capsera.agent decorator: sync and async functions, methods, nested scopes, and what to do when spend is attributed to unknown.

@capsera.agent() is the primary attribution API.

@capsera.agent("researcher", team="core", task_type="research")
def research(question: str) -> str:
    return client.messages.create(...)

Every LLM call made while that function is on the stack is attributed to researcher, including calls made by functions it calls and by frameworks it invokes.

Parameters

ParameterPurpose
nameRequired. The agent identifier shown in reports.
teamOwning team, for rolling spend up an organisation.
task_typeKind of work. Routing rules can match on this.
customer_idWhich of your customers to attribute spend to.
cost_centerFinance budget line.

Only name is required. The others can also come from an enclosing scope.

Supported function shapes

Async functions need no special handling. The decorator detects a coroutine function:

@capsera.agent("summarizer")
async def summarize(text: str) -> str:
    response = await client.messages.create(...)
    return response.content[0].text

Methods work, including when the client is on self:

class Triager:
    def __init__(self):
        self.client = anthropic.Anthropic()

    @capsera.agent("triager", team="support")
    def triage(self, ticket: str) -> str:
        return self.client.messages.create(...)

Generators and context managers used inside a decorated function are covered for the duration of the call, because attribution follows the call stack.

Nested scopes

Scopes nest, and the innermost value wins for each field independently:

@capsera.agent("pipeline", team="research")
def run(topic):
    plan = plan_work(topic)      # agent=pipeline,   team=research
    return write(plan)


@capsera.agent("writer", task_type="drafting")
def write(plan):
    ...                          # agent=writer, team=research, task_type=drafting

write overrides agent and adds task_type. It inherits team from run because it does not set one.

Where to apply the decorator

Apply it to the unit of work, not to every function that makes a call.

A useful test: if two decorated functions would always appear together in a report and you would never act on them separately, they are one agent. Decorating a private helper shared by three nodes splits one line item into the helper's line plus three empty ones.

For graph and crew frameworks, the unit is usually the node or agent:

from capsera import langgraph_node

@langgraph_node("planner")
def plan(state):
    ...

langgraph_node, crewai_agent, and crewai_task are aliases that open the same scope with framework-shaped parameter names. They record no framework field of their own — crewai_task sets task_type="crewai_task", and the rest set exactly what you pass. See Frameworks and Framework helpers.

Spend attributed to unknown

unknown is the agent for a recorded call with no attribution in scope. It is never dropped, because an unattributed cost is still a cost.

Common causes:

A call outside any decorated function. Module-level initialisation, a health check that pings a model, or a utility script.

A background task. contextvars propagate into asyncio tasks created inside a decorated scope. A task created elsewhere, such as a worker started at import time, has no scope to inherit.

A framework calling undecorated code. If a framework invokes a callable you did not decorate, the call happens outside your scopes. Use tag() inside the callable.

To make every call attributable, set a default at init and let decorators override it:

capsera.init(api_key=..., agent_name="api-server", team="platform")

Nothing is then attributed to unknown, and calls still landing on api-server are ones you have not yet named.

Next

Tags and sessions covers attribution narrower than a function, and grouping a conversation.

Manual record covers recording spend from a client the SDK does not patch.