SDK Reference¶
Complete API reference for the Coalex SDKs:
- Python SDK (
coalexv1.9.0) -- published on PyPI - TypeScript SDK (
@coalex-ai/sdkv1.7.0) -- published on npm
Installation¶
See Installation for all extras and framework-specific options.
Usage Pattern¶
Every Coalex integration follows the same pattern:
graph LR
A[register] --> B[auto_instrument]
B --> B2[declare_agent]
B2 --> C[coalex_context]
C --> D[evaluate]
D --> E[resolve]
1. Register -- connect to Coalex¶
Call once at application startup. Configures the global TracerProvider with an OTLP exporter pointing at your Coalex proxy.
2. Auto-instrument -- patch LLM frameworks¶
Automatically patches all installed LLM libraries (OpenAI, LangChain, LlamaIndex, etc.) to emit OpenInference spans.
3. Wrap with context -- tag every span¶
All spans created inside a coalex_context block inherit the agent metadata, making them queryable in the dashboard.
4. Declare agent -- register before traces¶
Pre-register your agent so the dashboard recognizes it before traces arrive.
5. Evaluate -- assess risk¶
Submit agent output for automated risk assessment. Low-risk outputs are auto-approved; high-risk ones are escalated for human review.
6. Resolve -- human-in-the-loop¶
When an output is escalated, a human reviewer approves, rejects, or corrects it.
Public API¶
Core Functions¶
| Function | Description | Reference |
|---|---|---|
register() |
Configure the OTLP exporter and global TracerProvider | Details |
declare_agent() |
Pre-register an agent before traces arrive | Details |
coalex_context() |
Create a parent span with agent metadata | Details |
auto_instrument() |
Patch all installed LLM frameworks | Details |
get_prompt() |
Fetch prompt templates from Prompt Vault (Python) | Details |
evaluate() |
Submit output for risk-based evaluation | Details |
resolve() |
Submit human review for an escalation | Details |
Data Classes¶
| Class | Description | Reference |
|---|---|---|
AgentDeclaration |
Result of declare_agent() |
Details |
PromptVersion |
Result of get_prompt() |
Details |
EvaluationDecision |
Result of evaluate() |
Details |
ResolutionResult |
Result of resolve() |
Details |
MetricResult |
Per-field metric scores | Details |
Extension Decorators / Wrappers¶
Instrument custom pipeline steps without LlamaIndex or LangChain. See Extensions.
| Python Decorator | TypeScript Wrapper | Span Kind | Reference |
|---|---|---|---|
@retrieval_span |
retrievalSpan() |
RETRIEVER |
Details |
@embedding_span |
embeddingSpan() |
EMBEDDING |
Details |
@reranker_span |
rerankerSpan() |
RERANKER |
Details |
@tool_span |
toolSpan() |
TOOL |
Details |
@guardrail_span |
guardrailSpan() |
GUARDRAIL |
Details |
Full Example¶
import os
import coalex
from coalex.ext.retrieval import retrieval_span, Document
from openai import OpenAI
# 1. Register
coalex.register(
endpoint=os.environ["COALEX_ENDPOINT"],
api_key=os.environ["COALEX_API_KEY"],
service_name="medical-agent",
)
# 2. Auto-instrument
coalex.auto_instrument()
# 3. Declare agent (recommended)
coalex.declare_agent(agent_id="medical-bot", display_name="Medical Bot")
# 4. Custom retrieval step
@retrieval_span(name="knowledge_base", query_arg="query")
def retrieve(query: str) -> list[Document]:
# Your retrieval logic here
return [Document(content="...", id="doc-1", score=0.95)]
# 5. Agent invocation
client = OpenAI()
with coalex.coalex_context(agent_id="medical-bot", request_id="req-456"):
docs = retrieve(query="What are the side effects?")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Based on: {docs[0].content}"}],
)
answer = response.choices[0].message.content
# 6. Evaluate
decision = coalex.evaluate(
request_id="req-456",
input={"question": "What are the side effects?"},
output={"answer": answer},
metrics={"answer": ["f1", "rouge_l"]},
)
# 7. Resolve (if escalated)
if decision.status == "escalated":
result = coalex.resolve(
escalation_id=decision.escalation_id,
decision="approved",
reviewer={"name": "Dr. Smith", "email": "dr.smith@hospital.org"},
)
import { register, coalexContext, autoInstrument, declareAgent, evaluate, resolve } from "@coalex-ai/sdk";
import { retrievalSpan, type Document } from "@coalex-ai/sdk/ext";
import OpenAI from "openai";
// 1. Register
register({
endpoint: process.env.COALEX_ENDPOINT!,
apiKey: process.env.COALEX_API_KEY!,
serviceName: "medical-agent",
});
// 2. Auto-instrument
autoInstrument();
// 3. Declare agent (recommended)
await declareAgent({ agentId: "medical-bot", displayName: "Medical Bot" });
// 4. Custom retrieval step
const retrieve = retrievalSpan(
{ name: "knowledge_base" },
async (query: string): Promise<Document[]> => {
// Your retrieval logic here
return [{ content: "...", id: "doc-1", score: 0.95 }];
},
);
// 5. Agent invocation
const client = new OpenAI();
await coalexContext({ agentId: "medical-bot", requestId: "req-456" }, async () => {
const docs = await retrieve("What are the side effects?");
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: `Based on: ${docs[0].content}` }],
});
const answer = response.choices[0].message.content;
// 6. Evaluate
const decision = await evaluate({
requestId: "req-456",
input: { question: "What are the side effects?" },
output: { answer },
metrics: { answer: ["f1", "rouge_l"] },
});
// 7. Resolve (if escalated)
if (decision.status === "escalated") {
const result = await resolve({
escalationId: decision.escalationId!,
decision: "approved",
reviewer: { name: "Dr. Smith", email: "dr.smith@hospital.org" },
});
}
});
API Reference¶
coalex ¶
Coalex Python SDK — AI governance observability.
Classes¶
AgentDeclaration
dataclass
¶
ConsoleAuthError ¶
Bases: Exception
A call did not carry a valid Console signature. Never says which check failed, to the caller.
The reason is available to the agent's own logs via reason; it must not travel back to
whoever made the call, or the error becomes an oracle for guessing a valid token.
Source code in coalex/console_auth.py
ConsoleCaller
dataclass
¶
Who the Console says this turn belongs to, once the signature has been verified.
Source code in coalex/console_auth.py
MissingExpectedAccountWarning ¶
Bases: UserWarning
verify_console_call() was called without expected_account (it becomes required in 2.0).
A subclass of UserWarning so CPython's default filters show it, and a named class so
silencing it is a typed one-liner::
warnings.filterwarnings("ignore", category=coalex.MissingExpectedAccountWarning)
Reported against the caller's frame, so a filter keyed on module names theirs, not ours.
Source code in coalex/console_auth.py
ConsoleKeys ¶
Bases: Mapping[str, str]
The Console's public keys by kid, fetched on demand and cached.
Reads are what verify_console_call does: kid in keys then keys[kid]. Both may fetch,
so both may block for up to timeout_seconds. In an async agent, call :meth:refresh at
startup — after that the steady state is a dict lookup, and a fetch only happens once per TTL or
when a key id is seen that is not held.
A fetch that fails leaves the previous keys in place. Serving a slightly stale key is better than refusing every call because the Console was briefly unreachable; a key that is genuinely gone stops verifying anyway, because the signature will not match.
Source code in coalex/console_jwks.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 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 | |
Methods:¶
refresh ¶
Fetch now, regardless of the TTL. Returns the keys held afterwards.
Never raises: a failure logs and keeps whatever was already cached. The caller cannot do anything useful with the exception — a missing key surfaces later as a refused token, which is the correct failure and the one the verifier already reports.
Source code in coalex/console_jwks.py
MissingAgentVersionWarning ¶
Bases: UserWarning
Raised by coalex_context() when no version was given.
A subclass of UserWarning so CPython's default filters show it, and a named class so
silencing it is a typed one-liner rather than a regex over our prose::
warnings.filterwarnings("ignore", category=coalex.MissingAgentVersionWarning)
module="coalex.*" does not work, and that is a consequence of getting the blame right: the
warning is reported against the caller's frame, so a filter keyed on the module names theirs.
Source code in coalex/context.py
ComposedQuestion
dataclass
¶
What a Question Composer decides the card shows (spec §6).
Everything the approver reads, and nothing else: the question, the curated summary, the values, and the schema that says how each value is answered. A composer that returns these four has decided the whole card.
Source code in coalex/decision_request.py
ContextPayload
dataclass
¶
What the human needs to judge (spec §4.1, §6 de-biasing).
Deliberately excludes the agent's reasoning, confidence, and risk score — those
stay in the trace for compliance and never reach the approval card. output is
shown as an editable answer, not as persuasion.
Source code in coalex/decision_request.py
DecisionRequest
dataclass
¶
The gateway-proof HITL seam (spec §4). See module docstring.
agent_display_name is the agent's display name. agent_name is its deprecated alias,
removed in coalex 2.0: it always holds the same value, is serialized beside it, and passing it
instead of agent_display_name warns once. When both are passed, agent_display_name wins,
so dataclasses.replace(dr, agent_display_name=...) changes both.
Source code in coalex/decision_request.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | |
Methods:¶
to_dict ¶
Serialize to the wire JSON the Console (and future Gateway) consume.
Keys are snake_case and mirror the TypeScript SDK's serializeDecisionRequest
exactly, so the seam is one shared contract across both SDKs.
Source code in coalex/decision_request.py
ElicitationAction ¶
Bases: StrEnum
Elicitation result action for a resolved task (spec §5).
accept = approved (optionally with content = corrections); decline =
rejected (+ reason, flows back to the agent as deny-with-instruction); cancel =
expired / timed out.
Source code in coalex/decision_request.py
ElicitationResult
dataclass
¶
The resolution of a task (spec §5, §7.2).
Carries the elicitation action plus the DORA who-did-what trail: reviewer
(SSO identity), reason, and timestamp. content holds corrections for an
accept-with-content; reason holds the decline instruction returned to the agent.
Source code in coalex/decision_request.py
IdentityComposer ¶
v1 (spec §6): the card shows what the producer sent, minus what §6 keeps out.
Deliberately does nothing interesting. Its whole value is that it is a SEAM — swapping it for a composer that renders a long document as a document, or annotates a recipient outside the organisation, changes the card with no change to the card's code.
The one thing it does do is drop the forbidden keys. That is not new behaviour for the Console,
which has always filtered them at render and still does; it is new for everyone else,
because until now an agent's reasoning or risk_score reached the wire and only one client
knew to look away.
Source code in coalex/decision_request.py
LegibleComposer ¶
Marks the fields a single-line input cannot show, so the card can render them as documents.
This is the composer the seam was built for, and the defect is real: a Connect IQ approval
carries a ~2,000-character markdown brief — headings, bullet lists, client and campaign
facts. Its JSON type is string, so the card falls through to its last branch and renders it
in a one-line <Input>. The approver is then asked to tick "Checked" against facts they were
never shown. An approval nobody can read is not governance; it is a rubber stamp that leaves an
audit trail.
It DECORATES another composer rather than replacing it, because legibility is orthogonal to
what the question says: LegibleComposer(GatewayComposer()) keeps the gateway's "Approve
calling 'wire_funds'…" question and still makes a long argument readable. Stacking is the
point — an all-or-nothing composer would force every deployment to reimplement the others.
It adds no content. §6 is not relaxed by making things legible: this changes how a value is SHOWN, never which values exist, and never adds a word of its own to the card.
Source code in coalex/decision_request.py
QuestionComposer ¶
Bases: Protocol
Turns an escalation into what the approver reads (spec §6).
The indirection exists so the card never renders the raw payload. A producer's output is shaped
for a machine — a 2,000-character markdown brief is a string, and the card, being
schema-driven, renders it in a single-line input the approver cannot read. Deciding that such a
value is a document and not a text field is this stage's job; there is nowhere else it belongs,
because the schema is the producer's and the card is generic.
Composers must not add what §6 keeps out. Making the facts LEGIBLE is the job; explaining them is not, and an agent's reasoning, confidence or risk score must not reach the card through a composed summary any more than through a field.
Source code in coalex/decision_request.py
ResolverPolicy
dataclass
¶
Who may resolve this request (spec §4.3): self or role:<name>.
Source code in coalex/decision_request.py
Methods:¶
parse
classmethod
¶
Parse the self | role:<name> config string into a policy.
Source code in coalex/decision_request.py
to_spec ¶
ResumeHook
dataclass
¶
How the producer resumes after resolution (spec §4.4).
v1 (SDK path): the LangGraph checkpoint-resume callback promoted to a first-class
field — the agent registers its resume url (and/or checkpoint_id) when it
escalates. Future (Gateway path): completing/failing the held MCP task_id.
Source code in coalex/decision_request.py
Methods:¶
from_dict
classmethod
¶
Rebuild a ResumeHook from its to_dict() wire shape (spec §4.4).
The inverse of to_dict — used when a persisted escalation carries a stored hook
(escalations.resume_hook) that must become a DecisionRequest. An
unrecognized kind falls back to checkpoint rather than raising, so a forward
Gateway value can never break the SDK read path.
Source code in coalex/decision_request.py
TaskState ¶
Bases: StrEnum
MCP task states carried by a Decision Request (spec §5).
working (agent in-flight, pre-escalation) and failed (technical error /
future Gateway path) have no current escalation-status equivalent; they exist so
the enum is complete for the shared Console+Gateway backbone.
Source code in coalex/decision_request.py
EvaluationDecision
dataclass
¶
Result of an evaluate() call.
Source code in coalex/evaluate.py
GuardResult
dataclass
¶
Outcome of :func:guard_tool_call.
decision is the final branch: allow (tool ran, result holds its return value),
escalate (paused pending a human — escalation_id is the Decision Request), or block
(denied; tool never ran).
Source code in coalex/gateway.py
AgentInventory
dataclass
¶
What an agent declares about itself.
An agent of a multi-agent system declares itself once per agent: the umbrella with the
endpoint the Console talks to and its resume hook, and each child with parent and no
endpoint, because a child is resumed through its nearest ancestor that has one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str | None
|
Where a turn is POSTed — the URL the Console will talk to. Optional: a child
agent that is only ever reached through its parent declares none, and the Console does
not offer it for a session. First, so a positional |
None
|
tools
|
Sequence[str]
|
The names of the tools this agent exposes. Names only: whether a tool is governed is the org's call, not the agent's. |
()
|
resume_url
|
str | None
|
Where a paused run is resumed, if the agent supports being paused. |
None
|
name
|
str | None
|
Deprecated alias of |
None
|
display_name
|
str | None
|
The agent's title, shown on a new chat in the Console. |
None
|
description
|
str | None
|
How the agent introduces itself on a new chat, shown as its first message. |
None
|
suggested_prompts
|
Sequence[str] | None
|
What an operator can ask it, shown as buttons that send that text. The Console serves every customer's agents, so it shows these as given and invents nothing when they are absent: an agent without them is shown by its name alone. |
None
|
agent_id
|
str | None
|
The agent's id, the one key the platform files its traces, policy and health
under. Omitted, it is the |
None
|
parent
|
str | None
|
The agent this one proposes as its parent. It governs nothing until the org confirms it in the dashboard; a re-declaration without it keeps the confirmed parent. |
None
|
An id is checked before anything is sent, by the rule the platform applies: it must not be
empty, start or end with whitespace, or be longer than 128 characters. Any character, .
included, is allowed, and its shape means nothing: a parent is only ever parent.
Source code in coalex/inventory.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
Methods:¶
payload ¶
The POST body. default_name is the id used when the declaration names none.
Source code in coalex/inventory.py
PromptVersion
dataclass
¶
A single prompt version returned by the Prompt Vault.
Source code in coalex/prompts.py
ResolutionResult
dataclass
¶
Result of a resolve() call.
Source code in coalex/resolve.py
Functions:¶
declare_agent ¶
declare_agent(
*,
agent_id: str,
display_name: str | None = None,
metadata: dict[str, Any] | None = None,
_config: CoalexConfig | None = None,
_api_key: str | None = None,
) -> AgentDeclaration
Register or declare an agent before traces arrive.
This is the building block for lazy agent registration. When the Prompt Vault's set_prompt() is implemented, it will call this internally to ensure the agent exists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id
|
str
|
Unique agent identifier (the merge key). |
required |
display_name
|
str | None
|
Optional human-friendly name. |
None
|
metadata
|
dict[str, Any] | None
|
Optional metadata (accepted but not persisted yet). |
None
|
_config
|
CoalexConfig | None
|
Override config (testing only). |
None
|
_api_key
|
str | None
|
Override API key (testing only). |
None
|
Source code in coalex/agents.py
instrument_anthropic ¶
instrument_bedrock ¶
instrument_google_genai ¶
Instrument Google GenAI (google-genai SDK) specifically.
instrument_langchain ¶
instrument_llamaindex ¶
instrument_openai ¶
instrument_vertexai ¶
verify_console_call ¶
verify_console_call(
token: str | None,
*,
audience: str,
public_keys: Mapping[str, str] | str,
expected_account: str | None = None,
leeway_seconds: int = DEFAULT_LEEWAY_SECONDS,
now: float | None = None,
) -> ConsoleCaller
Verify a Console-signed call, returning who it is for. Raises :class:ConsoleAuthError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str | None
|
The bearer token from the request, without the |
required |
audience
|
str
|
This agent's own name. A token minted for another agent is refused. |
required |
expected_account
|
str | None
|
The account this agent belongs to. A token the Console minted for any other
account is refused, even when it names this agent. Agent names are unique only within an
account and any organisation can declare its own agent endpoint, so the name alone does
not keep one organisation's Console out of another organisation's agent of the same
name; the account does. Omitting it keeps the old behaviour (the name alone) and warns
with :class: |
None
|
public_keys
|
Mapping[str, str] | str
|
|
required |
leeway_seconds
|
int
|
Clock skew tolerance. |
DEFAULT_LEEWAY_SECONDS
|
now
|
float | None
|
Override the clock, for tests. |
None
|
The order matters: the signature is checked before any claim is read. Claims from an
unverified token are attacker-controlled strings, and reading them first — even to pick a key —
is how verification gets subverted. Only kid, from the header, selects the key, and an
unknown kid is a refusal rather than a fallback to some default key.
Source code in coalex/console_auth.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | |
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"))
Source code in coalex/context.py
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 | |
current_agent_id ¶
Return the agent_id of the active coalex_context(), or None outside one.
evaluate() reads it so a mid-run escalation names its agent before the trace is ingested.
current_agent_version ¶
current_parent_agent_id ¶
Return the agent whose context the active one opened inside, or None for a top-level agent.
current_request_id ¶
Return the request_id of the active coalex_context(), given or carried from an outer one.
current_run_id ¶
from_escalation ¶
from_escalation(
escalation: Mapping[str, Any] | Any,
*,
resolver: ResolverPolicy | str | None = None,
resume_hook: ResumeHook | None = None,
question: str | None = None,
composer: QuestionComposer | None = None,
) -> DecisionRequest
Materialize an escalation as a Decision Request (spec §4).
Accepts the escalation as the API JSON dict or any object with matching attributes
(e.g. the transformer's EscalationItem). The task state and elicitation action
follow spec §5's mapping. The escalation has no resolver/resume-hook of its own, so
the producer supplies them here; resolver defaults to self.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
escalation
|
Mapping[str, Any] | Any
|
The escalation dict/object (status + fields). |
required |
resolver
|
ResolverPolicy | str | None
|
Resolver policy or its |
None
|
resume_hook
|
ResumeHook | None
|
How the producer resumes after resolution (spec §4.4). |
None
|
composer
|
QuestionComposer | None
|
Decides what the card shows (spec §6). Default :class: |
None
|
question
|
str | None
|
Overrides the composed question. Kept for callers that passed one before the composer existed; a composer is the better place to decide it. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the escalation carries an unrecognized status. |
Source code in coalex/decision_request.py
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 | |
resume_body_from_result ¶
Map a resolved ElicitationResult → the JSON body an agent's resume endpoint expects.
The exact wire shape the working PharmaConnect resume path reads back (pilot-bluepharma-v2
agent/src/runtime/api.py ResumeRequest + domain/approval.py request_approval):
{"decision", "corrections", "reason", "reviewer_name", "reviewer_email"}
Mapping from ElicitationAction (spec §5):
ACCEPTwith nocontent→decision="approved"ACCEPTwithcontent→decision="corrected",corrections=contentDECLINE→decision="rejected",reasoncarried through verbatim (deny-with-instruction)CANCEL→decision="expired"
Pure: no I/O. The resolve-trigger POSTs this to resume_hook.url to resume the run.
Source code in coalex/decision_request.py
task_state_for_status ¶
Map an escalation status to its MCP task state (spec §5).
Source code in coalex/decision_request.py
decide_policy ¶
decide_policy(
*,
governed: bool,
risk_tier: str | None = None,
trust_score: float | None = None,
blocked: bool = False,
) -> PolicyDecision
Pure policy decision: (governed, risk_tier, trust_score) → allow | escalate | block.
Deterministic and side-effect-free. trust_score is the 0-100 dial. Rules:
blocked(orrisk_tier == "blocked") → block — never runs.- not
governed→ allow — ungoverned tools always pass (Scene 1 / YOLO). - governed +
risk_tier == "high"→ escalate — a high-risk (destructive) tool always needs a human, regardless of trust; no auto-approve on that path. - governed +
trust_score >= HIGH_TRUST_MIN→ allow — earned autonomy (Scene 4). - governed, otherwise → escalate — the default governed behaviour (Scene ⅔).
Source code in coalex/gateway.py
guard_tool_call ¶
guard_tool_call(
*,
tool_name: str,
arguments: dict[str, Any],
governed: bool,
run: Callable[[], Any],
request_id: str,
agent_id: str | None = None,
risk_tier: str | None = None,
trust_score: float | None = None,
resolver: str = "self",
blocked: bool = False,
resume_hook: ResumeHook | None = None,
_config: CoalexConfig | None = None,
_api_key: str | None = None,
) -> GuardResult
Wrap one tool call in the Gateway v0 policy. Same agent, same prompt — the config
(the governed flag) decides whether the wire executes or becomes an escalation.
- allow → runs
run()and records an allowed-call audit row; returns its result. - escalate → does NOT run the tool; calls the existing SDK
evaluate()(creating a pending Decision Request the Console resolves; pause/resume ridesresume_hook+ the resolve callback, exactly as PharmaConnect does today). If the risk engine instead auto-approves, the tool runs; if it rejects, the tool is blocked. - block → does NOT run the tool; records a blocked-call audit row.
run is a zero-arg callable that executes the underlying tool (so allow/auto-approved actually
invoke it, while escalate/block never do). It must be synchronous: an async callable would
return an un-awaited coroutine, and this raises rather than record a call that never happened.
This fails closed, and that is deliberate. If Coalex is unreachable at the moment
a decision is needed — the escalate path calls evaluate(), which raises on a transport
error or a non-2xx — the exception propagates and your tool does not run. A governance
layer that waves calls through when it cannot reach its own control plane is not governing.
The behaviour was always this; what was missing was anyone saying so. A developer saw a raw
httpx.ConnectError and had to infer, from a stack trace, that their governed action had been
prevented. It is now logged in those words before the exception leaves this function.
Source code in coalex/gateway.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 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 | |
declare_inventory ¶
declare_inventory(
inventory: AgentInventory,
*,
config: CoalexConfig,
api_key: str,
default_name: str,
) -> bool
POST one declaration with an explicit config and key. Returns whether it landed; never raises.
coalex.declare() does the same with what register() configured. default_name is
treated as an explicitly passed service_name: the id when neither the declaration nor the
active context names one (deprecated), and never a default service name.
register() runs at application startup, so this must not be able to stop an agent from
starting. A governance registration failing is a reason to log loudly, not a reason for a
customer's agent to be down — the agent still traces, and the declaration retries on next boot.
Source code in coalex/inventory.py
get_prompt ¶
get_prompt(
*,
agent: str,
name: str,
version: str | int = "production",
_config: CoalexConfig | None = None,
_api_key: str | None = None,
) -> PromptVersion
Fetch a prompt template from the Prompt Vault.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent
|
str
|
Agent ID (e.g., "sales-copilot"). |
required |
name
|
str
|
Prompt name (e.g., "system"). |
required |
version
|
str | int
|
"production" (default), "draft", "latest", or a version number.
Anything else raises |
'production'
|
_config
|
CoalexConfig | None
|
Override config (testing only). |
None
|
_api_key
|
str | None
|
Override API key (testing only). |
None
|
Source code in coalex/prompts.py
flush_ungoverned ¶
Deliver what the spool is holding. Returns how many landed. Never raises.
Called after a SUCCESSFUL platform call as well as from record, because success is the only
honest signal that the platform is back. A spool that only drains when the next failure happens
would keep the backlog exactly as long as the outage lasts, and then some.
Source code in coalex/ungoverned.py
record_ungoverned ¶
record_ungoverned(
*,
request_id: str,
action: str,
error: str,
_config: Any = None,
_api_key: str | None = None,
) -> None
Record that action ran without governance, because the platform could not be reached.
Call it from the except around evaluate(), beside the decision to proceed. It does not
change that decision and cannot fail it.
Delivery is attempted immediately and spooled to disk if it does not land, which is the usual case: the reason this is being called is that the platform was unreachable a moment ago.
Source code in coalex/ungoverned.py
register ¶
register(
*,
endpoint: str = "https://traces.coalex.ai",
api_key: str = "dev",
service_name: str | None = None,
filter_non_ai_spans: bool = False,
suppress_export_errors: bool = True,
genai_semconv: bool | None = None,
capture_content: bool = True,
prompt_refs: bool | None = None,
declare: AgentInventory
| Sequence[AgentInventory]
| None = None,
) -> None
Configure the Coalex OTLP exporter.
Call once at application startup. Sets the global TracerProvider with a BatchSpanProcessor exporting OTLP/HTTP to the Coalex proxy.
The SDK always sends through the proxy (via LB) so that auth validation is in the path. Never send directly to the collector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
Coalex proxy HTTP endpoint (default: cloud). |
'https://traces.coalex.ai'
|
api_key
|
str
|
API key for proxy authentication (default: "dev" for local). |
'dev'
|
service_name
|
str | None
|
Service name for OTLP resource (default: "coalex-sdk-python"). It is not an
agent id: a declaration without |
None
|
filter_non_ai_spans
|
bool
|
If True, only export AI/LLM spans (default: False). |
False
|
suppress_export_errors
|
bool
|
If True, suppress export errors gracefully (default: True). |
True
|
genai_semconv
|
bool | None
|
If True, additively emit OTel GenAI attributes
( |
None
|
capture_content
|
bool
|
If False, export spans without their content: the prompts, the
responses, |
True
|
prompt_refs
|
bool | None
|
If True (the default), the text of a prompt fetched with :func: |
None
|
declare
|
AgentInventory | Sequence[AgentInventory] | None
|
Optionally tell the platform where this agent lives and what tools it has,
so it appears in the Console without anyone registering it by hand.
One :class: |
None
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The options cannot take effect. Either |
A tracer provider the application installed first (its own OpenTelemetry setup, Logfire,
opentelemetry-instrument) is kept: the SDK adds its attribute processor and its exporter to
it, so capture_content and the other options apply to what Coalex receives. The
application's other exporters on that provider still see every span unmodified, and the
provider keeps its own resource, so service_name does not rename it.
Source code in coalex/__init__.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 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 | |
declare ¶
Declare one agent, or several, with the endpoint and key register() configured.
For an agent that declares itself after startup, or declares its children once it knows them. Each declaration is checked locally and sent on its own, so one refusal does not stop the others; each refusal is logged with its reason. Never raises for a refused or failed declaration.
Returns:
| Type | Description |
|---|---|
bool | list[bool]
|
Whether the declaration landed, or a list of that, in order, when given a list. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
|