coalex_context()¶
Context manager that creates a coalex.invocation parent span. All spans created inside this block are tagged with the agent metadata, making them queryable by agent_id, request_id, and version in the Coalex dashboard.
Signature¶
interface CoalexContextOptions {
agentId: string;
requestId?: string;
runId?: string;
version?: string;
promptVersion?: number;
promptType?: string;
}
async function coalexContext<T>(
options: CoalexContextOptions,
fn: () => Promise<T> | T,
): Promise<T>
Callback pattern
The TypeScript SDK uses a callback pattern instead of Python's with statement. Pass your logic as an async function to coalexContext(). A synchronous variant coalexContextSync() is also available.
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
agent_id |
str |
required | Identifier for the AI agent. Used to group all traces from the same agent in the dashboard. |
request_id |
str \| None |
None |
Your correlation key for one unit of business work. It may span traces, resumes and agents. Carried from an enclosing context when omitted. |
version |
str \| None |
None |
Agent version string (e.g., "1.2.0", "canary"): the build this invocation is running. Omitting it warns once per agent; it becomes required in coalex 2.0 / @coalex-ai/sdk 3.0. |
prompt_version |
int \| None |
None |
Prompt Vault version driving this invocation. |
prompt_type |
str \| None |
None |
Prompt type (e.g., "system"). |
run_id |
str \| None |
None |
One execution of an agent graph, across interrupts and resumes. Carried from an enclosing context when omitted. Never derive it from a framework's thread, session or conversation id. |
All parameters are keyword-only (enforced by *).
Returns¶
A context manager that yields None. Use it with with statements.
Span Attributes¶
The coalex.invocation parent span carries these attributes:
| Attribute | Set When | Value |
|---|---|---|
coalex.agent_id |
Always | Value of agent_id parameter |
coalex.parent_agent_id |
Opened inside a context with a different agent | The enclosing agent's agent_id |
coalex.request_id |
Given, or carried from an enclosing context | The request id |
coalex.run_id |
Given, or carried from an enclosing context | The run id |
coalex.agent_version |
Given, or inherited from the same agent | Value of version parameter |
coalex.prompt_version / coalex.prompt_type |
Given, set by get_prompt(), or inherited from the same agent |
The prompt version and type |
It also carries the OpenInference and OpenTelemetry GenAI conventions for an agent run, so a third-party backend (Phoenix, Langfuse, any GenAI-convention reader) shows it as an agent:
| Attribute | Value |
|---|---|
openinference.span.kind |
AGENT |
gen_ai.operation.name |
invoke_agent |
gen_ai.agent.name |
Value of agent_id parameter |
These three are on coalex.invocation only and are not propagated: a descendant is a model call, a tool or a chain. gen_ai.agent.id is not set, because the OpenTelemetry convention reserves it for hosted agents.
Attribute propagation
The CoalexAttributePropagator (configured by register()) puts the coalex.* attributes on every span started while the context is active: every LLM call, retrieval and tool invocation inside the block carries the agent metadata without manual tagging. The values come from the active context first, and from the parent span only outside any context. A framework instrumentor such as OpenInference LangChain parents its spans on its own run tree, so a span's parent can belong to an outer agent while an inner context is active; the context decides. A value a span already carries is never overwritten.
How It Works¶
- Obtains a tracer named
"coalex"from the globalTracerProvider. - Starts a new span named
"coalex.invocation"as the current span. - Sets the
coalex.*attributes on that span. - All spans created inside the
withblock become children of thecoalex.invocationspan. - When the block exits, the span ends automatically: OK on success, ERROR with an
exceptionevent on an exception, and OK on a human-approval pause.
graph TD
A["coalex.invocation<br/>(coalex.agent_id=support-bot)"] --> B["openai.chat.completions<br/>(auto-instrumented)"]
A --> C["knowledge_base<br/>(@retrieval_span)"]
C --> D["embed<br/>(@embedding_span)"]
Examples¶
Basic usage¶
import coalex
from openai import OpenAI
coalex.register(endpoint="https://traces.coalex.ai", api_key="your-key")
coalex.auto_instrument()
client = OpenAI()
with coalex.coalex_context(agent_id="support-bot", request_id="req-001"):
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "What is the refund policy?"}],
)
print(response.choices[0].message.content)
import { register, coalexContext, autoInstrument } from "@coalex-ai/sdk";
import OpenAI from "openai";
register({ endpoint: "https://traces.coalex.ai", apiKey: "your-key" });
autoInstrument();
const client = new OpenAI();
await coalexContext({ agentId: "support-bot", requestId: "req-001" }, async () => {
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "What is the refund policy?" }],
});
console.log(response.choices[0].message.content);
});
With version tracking¶
await coalexContext({
agentId: "claims-processor",
requestId: "claim-2024-0042",
version: "2.1.0",
}, async () => {
// All spans inside here carry:
// - coalex.agent_id = "claims-processor"
// - coalex.request_id = "claim-2024-0042"
// - coalex.agent_version = "2.1.0"
const result = await processClaim(claimData);
});
Nested contexts¶
You can nest contexts when one agent delegates to another in the same process. The inner context creates its own coalex.invocation span as a child of the outer one, and every span inside it belongs to the inner agent, in both SDKs:
- The inner context wins for every value it is given.
- A different agent gets
coalex.parent_agent_idset to the enclosing agent, on itscoalex.invocationand every descendant. It carriesrequest_idandrun_idfrom the enclosing context unless given, and inherits neitherversionnor the prompt: those belong to the enclosing agent, so pass the worker's own. - The same agent nested inherits everything and sets no
coalex.parent_agent_id. - When the inner context exits, the enclosing one is active again.
with coalex.coalex_context(agent_id="orchestrator", request_id="req-100", run_id="run-7", version="2.0.0"):
# coalex.invocation: agent_id=orchestrator
with coalex.coalex_context(agent_id="sub-agent-a", version="1.3.0"):
# coalex.invocation: agent_id=sub-agent-a, parent_agent_id=orchestrator,
# request_id=req-100, run_id=run-7, agent_version=1.3.0
run_sub_agent_a()
with coalex.coalex_context(agent_id="sub-agent-b", version="0.9.1"):
run_sub_agent_b()
await coalexContext({ agentId: "orchestrator", requestId: "req-100", runId: "run-7", version: "2.0.0" }, async () => {
// coalex.invocation: agent_id=orchestrator
await coalexContext({ agentId: "sub-agent-a", version: "1.3.0" }, async () => {
// coalex.invocation: agent_id=sub-agent-a, parent_agent_id=orchestrator,
// request_id=req-100, run_id=run-7, agent_version=1.3.0
await runSubAgentA();
});
await coalexContext({ agentId: "sub-agent-b", version: "0.9.1" }, async () => {
await runSubAgentB();
});
});
The TypeScript context lives in one AsyncLocalStorage store, so concurrent invocations of different agents in one process each keep their own agent, version and prompt. coalex.parent_agent_id is what the agent claims about its parent: it is displayed, and only a parent the organisation confirms governs.
One agent per LangGraph node¶
agent_node / agentNode wraps a LangGraph node function so everything the node does belongs to its own agent. Wrap the node, not the graph's invoke: LangGraph re-executes the node from the top on every resume, so the context opens again for the resumed work, and the node's checkpoint namespace is unchanged. The wrapper keeps the function's signature (LangGraph inspects it to pass config) and passes every argument through.
graph.add_node("w1_rfq", coalex.agent_node("rfq-worker", rfq_worker, version="1.4.0"))
graph.add_node("w2_quote", coalex.agent_node("quote-worker", quote_worker, version="1.4.0"))
app = graph.compile(checkpointer=checkpointer)
with coalex.coalex_context(agent_id="orchestrator", request_id=request_id, run_id=run_id, version="2.0.0"):
app.invoke(state, config)
Sync and async node functions are both supported.
graph.addNode("w1_rfq", agentNode("rfq-worker", rfqWorker, { version: "1.4.0" }));
graph.addNode("w2_quote", agentNode("quote-worker", quoteWorker, { version: "1.4.0" }));
const app = graph.compile({ checkpointer });
await coalexContext({ agentId: "orchestrator", requestId, runId, version: "2.0.0" }, () =>
app.invoke(state, config),
);
A pause is not an error¶
LangGraph pauses for a human (interrupt()) and hands off to a parent graph (Command(graph=Command.PARENT)) by raising an exception through the node. When the exception passing through a coalex.invocation is a GraphBubbleUp, GraphInterrupt or ParentCommand (or a subclass, such as NodeInterrupt), the span ends OK with no exception event, and the exception is re-raised unchanged so LangGraph still pauses. Every other exception still ends the span ERROR. The spans of the tool, guardrail and retrieval decorators (tool_span, guardrail_span, retrieval_span in Python; toolSpan, guardrailSpan, retrievalSpan in TypeScript) follow the same rule, so a run waiting for an approval inside a decorated tool shows an OK tool span, not a failed one; a paused guardrail records no guardrail.result. The Python embedding and reranker spans do the same. The SDKs recognise the three by class name (Python: anywhere in the MRO; TypeScript: the error's name or any class in its prototype chain), so neither depends on LangGraph.
Reading the active context¶
Both SDKs expose the active context's values, for example to put them in your own logs or to pass them to a call that runs outside the context. Each returns None / undefined outside a context.
| Python | TypeScript | Returns |
|---|---|---|
coalex.current_agent_id() |
currentAgentId() |
The active agent |
coalex.current_parent_agent_id() |
currentParentAgentId() |
The enclosing agent, for a context nested in a different agent |
coalex.current_request_id() |
currentRequestId() |
The request id, given or carried |
coalex.current_run_id() |
currentRunId() |
The run id, given or carried |
coalex.current_agent_version() |
currentAgentVersion() |
The active agent's version |
Generating request IDs¶
Use uuid4 to generate unique request IDs:
Notes¶
coalex_context()requiresregister()to have been called first (so a globalTracerProviderexists). Ifregister()has not been called, spans are created on a no-op tracer and silently discarded.- The context manager is not reentrant -- do not reuse a single
coalex_contextinstance. Create a new one for each invocation. - The
request_iddoes not need to be globally unique, but it should be unique within a reasonable time window (e.g., per user session or per API request).
API Reference¶
coalex.context.coalex_context ¶
coalex_context(
*,
agent_id: str,
request_id: str | None = None,
version: str | None = None,
prompt_version: int | None = None,
prompt_type: str | None = None,
run_id: str | None = None,
replay_id: str | None = None,
case_id: str | None = None,
case_timestamp: datetime | None = None,
) -> Generator[None, None, None]
Create a parent span with Coalex attributes for all child spans.
All spans created inside this context are descendants of a
coalex.invocation span that carries the agent metadata.
This makes every child span queryable by agent_id, request_id,
and version in the Bronze layer.
The coalex.invocation span also carries openinference.span.kind=AGENT,
gen_ai.operation.name=invoke_agent and gen_ai.agent.name=<agent_id>, so third-party
backends show it as an agent run; its descendants do not.
Context vars are set before the span is created, and CoalexAttributePropagator reads them
before the parent span: a framework instrumentor (OpenInference LangChain) parents spans on its
own run tree, so a span's parent can belong to an outer agent while this context is active.
Nesting. An inner context wins for every value it is given. Opened inside a context with a
different agent, it sets coalex.parent_agent_id to that agent, carries request_id and
run_id, and inherits neither version nor the prompt. Inside the same agent it inherits
everything and sets no parent. replay_id, case_id and case_timestamp are carried into
every nested context, whichever its agent.
Replay. A historical case re-run by a replay passes replay_id, case_id and
case_timestamp; every span inside then carries coalex.replay_id, coalex.case_id and
coalex.case_timestamp (ISO 8601 with offset), which tie it to its case and keep replay traffic
apart from live traffic. Span start times stay the real run time. Outside a replay none of the
three is set.
A pause is not an error. An exception whose class, or any base class, is named
GraphBubbleUp, GraphInterrupt or ParentCommand (LangGraph's interrupt() and
Command(graph=Command.PARENT)) ends the span OK with no exception event and is re-raised.
Any other exception ends it ERROR.
Do not wrap a generator that is consumed after the enclosing function returns. A streaming HTTP response is the usual case: the body generator runs once the handler has already returned, so this block exits in a different Context from the one it entered, and the variables cannot be restored. Enter the context inside the generator instead, so its lifetime matches the work it describes::
async def body():
with coalex_context(agent_id=..., request_id=...):
async for token in agent.stream(...):
yield token
Getting this wrong no longer raises — it leaks a context var and logs at debug — but the spans will be attributed to whatever context happens to be current, which is rarely what you meant.
prompt_version / prompt_type let an agent that fetches its prompt
once at startup (outside any request trace) attribute each request to a
prompt version. get_prompt() only stamps the trace it is called in, so
the cache-at-startup pattern needs the version passed here per request.
When omitted, any value already set by get_prompt() is left
untouched, except inside another agent's context: that prompt is not this agent's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id
|
str
|
Identifier for the AI agent. |
required |
request_id
|
str | None
|
The customer's correlation key for one unit of business work. Carried from an enclosing context when omitted. |
None
|
version
|
str | None
|
Agent version string — the build this invocation is running. Optional today,
and omitting it emits a |
None
|
prompt_version
|
int | None
|
Prompt Vault version driving this invocation. |
None
|
prompt_type
|
str | None
|
Prompt type (e.g. |
None
|
run_id
|
str | None
|
One execution of an agent graph, across interrupts and resumes. Carried from an enclosing context when omitted. Never derive it from a framework's thread or session id. |
None
|
replay_id
|
str | None
|
The replay run this invocation belongs to. Carried from an enclosing context when omitted. |
None
|
case_id
|
str | None
|
The historical case being replayed. Carried from an enclosing context when omitted. |
None
|
case_timestamp
|
datetime | None
|
When the original case happened. Timezone-aware; a naive datetime raises
|
None
|
Source code in coalex/context.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | |
coalex.context.agent_node ¶
Wrap a LangGraph node function so everything it does belongs to agent_id.
Wrap the node function, not the graph's invoke: LangGraph re-executes the node from the
top on every resume (Command(resume=...)), so the context opens again for the resumed work,
and the node's checkpoint namespace is unchanged. The context carries the enclosing
request_id and run_id, names the enclosing agent as coalex.parent_agent_id, and a
pause (interrupt()) leaves its span OK.
Works for sync and async node functions, and keeps the function's signature, which LangGraph
inspects to decide whether to pass config::
graph.add_node("w1_rfq", coalex.agent_node("rfq-worker", rfq_worker, version="1.4.0"))