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
| Parameter | Purpose |
|---|---|
name | Required. The agent identifier shown in reports. |
team | Owning team, for rolling spend up an organisation. |
task_type | Kind of work. Routing rules can match on this. |
customer_id | Which of your customers to attribute spend to. |
cost_center | Finance 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.