TokenPolice
Docs
SDK reference

Wrapping your app

session, agent, chain and workflow — when to use which — plus dynamic identity binding.

Wrapping an entrypoint does two things: it groups every LLM call inside into one trace, and it attaches identity (user_id, paid_plan, session_id) so your rules can target that traffic. You get the same tagging four ways — pick by shape.

The four wrappers

WrapperShapeRecorded as
workflowDecorator (Python) / higher-order function (Node) — wrap a whole entrypointa chain
sessionA block you open around some workan agent
agentA session recorded as an agent — a dynamic, LLM-driven loopan agent
chainA session recorded as a chain — a fixed, linear pipelinea chain

"Recorded as" is just the label the run carries in the dashboard: agent for work that decides its own next step, chain for a fixed pipeline. It changes nothing about behaviour — pass kind="agent" / kind="chain" to workflow or session (or use agent/chain) if the default label doesn't fit the shape of your code.

Reach for workflow first — it's the least intrusive. Use session / agent / chain when you want to wrap a region of code rather than a whole function, or to be explicit about whether the run is a dynamic agent or a linear chain.

workflow

Wrap the function that handles one request or run. Every LLM call inside is tagged.

import token_police as tp

@tp.workflow(name="support_agent")
def handle(user_id: str, paid_plan: str, session_id: str, query: str):
    ...   # all LLM calls here share one trace + this identity

Node — same idea, as a wrapper:

import * as tp from "token-police";

const handle = tp.workflow({ name: "support_agent" }, async (req) => {
  // ...
});

session / agent / chain

Open a block around the work instead of wrapping a function:

with tp.session(name="nightly_report", user_id="batch", kind="chain"):
    ...   # calls in here belong to this session
await tp.session({ name: "nightly_report", userId: "batch" }, async () => {
  // ...
});

tp.agent(...) and tp.chain(...) are the same call with the anchor fixed — use agent for a loop that decides its own steps, chain for a set pipeline. Nested scopes inherit the parent's session_id and trace unless you pass new values.

Dynamic identity: bind_args

Often you don't know user_id or session_id until the request arrives. workflow can read them straight off your function's arguments — on by default.

  • Python (bind_args=True) inspects the function signature and picks up arguments named user_id, paid_plan, session_id, workflow_name, or metadata. Each is used only if it's the right type (strings; metadata a dict); otherwise the static decorator value stands. Any binding failure falls back silently to the static values.
  • Node (bindArgs: true) reads userId, paidPlan, sessionId and metadata off the first argument when it's a plain object (JS has no signature introspection). The snake_case spellings user_id, paid_plan and session_id are accepted too. A plain-string first argument is deliberately not auto-bound.
# user_id + session_id come from the call, not hard-coded
@tp.workflow(name="chat")
def chat(user_id: str, session_id: str, message: str): ...

To thread a multi-turn conversation into one trace, pass the same session_id on every turn. The full contract is on Identity.

Next