Skip to main content

Python SDK reference

acruxcore is the async Python SDK for AcruxCore. It renders stored prompts, calls the gateway (with streaming, tools, and structured output), runs the tool-calling loop, reports and reads traces, and manages the tool catalog. Every method below has a 1:1 Node counterpart in @acruxcoreai/sdk — only the casing and a few option shapes differ.

pip install acruxcore

The SDK is async throughout, so AcruxCore is an async context manager. Create one instance at startup and reuse it.

Construct the client

import asyncio
from acruxcore import AcruxCore

async def main():
# Reads ACRUXCORE_API_KEY and ACRUXCORE_BASE_URL from the environment.
async with AcruxCore() as hub:
...

asyncio.run(main())
ParameterTypeDefaultNotes
api_keystrACRUXCORE_API_KEYRaises MISSING_API_KEY if neither is set.
base_urlstrACRUXCORE_BASE_URLRaises MISSING_BASE_URL if neither is set.
cache_ttlint (ms)60000Render cache window. 0 disables caching (and serve-stale).
max_cache_sizeint500Max LRU entries. Set by the first instance.
max_retriesint1Retries on transient failure (2 total attempts).
retry_intervalint (ms)500Delay between retries.
timeoutfloat (s)30.0Per-request timeout.
transporthttpx.AsyncBaseTransportFor testing/injection.
providerProviderConfigClient-level BYO default; overridden by a per-call provider.

Errors raise AcruxCoreError with a machine-readable code — see Error codes.

prompts.render(name, alias, variables=None)

Render a stored prompt by name + alias into templated messages, plus the tools attached to that version. Cached per (api_key, name, alias, variables) with stale-while-revalidate: a fresh hit returns immediately, a stale hit returns immediately and refreshes in the background, a cold miss fetches.

rendered = await hub.prompts.render(
"support-reply", "production",
{"company": "Acme", "customer_message": "Order #123 is late"},
)
print(rendered.messages, rendered.model)
ParameterTypeRequiredNotes
namestryesPrompt slug (not id).
aliasstryese.g. "production", "staging".
variablesdictnoTemplate variables. Defaults to {}.

Returns RenderResult(messages, tools, model, version_id, version_number, variables). model is the version's bound default (or None); variables echoes back what you called with. Pass version_id as prompt_version_id and variables as variables to gateway.chat()/gateway.run_tool_loop() — the pair is what gives a trace prompt lineage and lets feedback on it become an evaluation dataset example. gateway.run_prompt_with_tools(rendered) does both for you. Raises MISSING_VARIABLES if the template needs a variable you did not supply.

Prompt lifecycle (hub.prompts)

prompts.list(*, search=None, page=None, limit=None)

List prompts for the team, newest first.

page = await hub.prompts.list(search="support", limit=10)
ParameterTypeNotes
searchstrFree-text search on name.
pageint1-based page.
limitintPage size.

Returns PromptListResult(data, total, page, limit).

prompts.get(prompt_id)

Fetch one prompt by id.

prompt = await hub.prompts.get(prompt_id)

Returns PromptDetail (id, name, description, version_count, created_at, updated_at).

prompts.create(name, *, description=None)

Create a new prompt shell. Commit a version with commit_version to give it content.

prompt = await hub.prompts.create("support-bot", description="Customer support")

Returns PromptDetail.

prompts.update(prompt_id, **kwargs)

Update a prompt's name and/or description. Does not touch versions.

updated = await hub.prompts.update(prompt_id, description="v2 description")

Returns PromptDetail.

prompts.delete(prompt_id)

Delete a prompt and every version/alias under it. Returns None.

await hub.prompts.delete(prompt_id)

prompts.commit_version(prompt_id, messages, *, model=None, tools=None)

Commit a new immutable version for a prompt. The first commit auto-creates production and staging aliases.

version = await hub.prompts.commit_version(
prompt_id,
messages=[
{"role": "system", "content": "You are a support agent."},
{"role": "user", "content": "{{question}}"},
],
model="gpt-4o-mini",
)

Returns VersionDetail (id, version_number, messages, model, tools, aliases). aliases is present only on the first version.

prompts.list_versions(prompt_id, *, page=None, limit=None)

List a prompt's versions, newest first. Items omit messages — use get_version for full content.

versions = await hub.prompts.list_versions(prompt_id)

Returns VersionListResult(data, total, page, limit).

prompts.get_version(prompt_id, version_number)

Fetch one version with its full messages.

v = await hub.prompts.get_version(prompt_id, 1)

Returns VersionDetail.

prompts.diff(prompt_id, from_version, to_version)

Diff two versions.

diff = await hub.prompts.diff(prompt_id, 1, 2)

Returns DiffResult (changes).

prompts.promote_alias(prompt_id, alias, version_number)

Point an alias (e.g. "production") at a specific version. Creates the alias if it doesn't exist.

alias = await hub.prompts.promote_alias(prompt_id, "production", 3)

Returns AliasDetail (alias, version_number, prompt_id).

prompts.list_aliases(prompt_id)

Read every alias on a prompt and the version each one points at — the read half of promote_alias, for a deploy check or a CI guard asking "which version is production on right now?".

for a in await hub.prompts.list_aliases(prompt_id):
print(a.alias, "->", f"v{a.version_number}")

Returns list[AliasDetail]. Empty for a prompt with no committed version, since the first version is what mints the aliases. Added in 0.10.0.

prompts.export_version(prompt_id, version_number)

Export a version for portability (JSON blob).

exported = await hub.prompts.export_version(prompt_id, 1)

Returns ExportedPromptVersion.

prompts.import_prompt(export_data)

Import an exported prompt as a new prompt with one version.

imported = await hub.prompts.import_prompt(exported)

Returns ImportPromptResult (prompt_id, prompt_name, version_number).

prompts.traces_for_version(prompt_id, version_number, *, limit=None, page=None)

List traces that used a specific prompt version.

traces = await hub.prompts.traces_for_version(prompt_id, 1, limit=10)

Returns TraceListResult(data, total, page, limit).

gateway.chat(...)

One gateway completion at POST /gateway/chat/completions — no tool-dispatch loop. If the model returns tool_calls, they are handed back raw on result.message["tool_calls"]; use gateway.run_tool_loop to dispatch them.

reply = await hub.gateway.chat("support-model", rendered.messages, temperature=0.2)
print(reply.content, reply.usage)

For streaming, use gateway.stream() which returns an async iterator of chunks:

stream = await hub.gateway.stream("support-model", rendered.messages)
async for chunk in stream:
print(chunk.delta.get("content", ""), end="", flush=True)
ParameterTypeRequiredNotes
modelstryesModel public name.
messageslist[dict]yesChat messages.
toolslist[ToolDefinition]noInline OpenAI-shaped tool definitions.
tool_refslist[{name, alias?}]noCatalog tool references (resolved server-side).
tool_choiceToolChoicenoHow the model uses tools.
response_formatResponseFormatnoStructured output. Mutually exclusive with tools/tool_choice.
temperaturefloatnoSampling temperature.
max_tokensintnoMax completion tokens.
streamboolnoReturn an async iterator of ChatChunk instead of ChatResult.
providerProviderConfignoPer-call BYO override.
prompt_version_idstrnoFrom prompts.render().version_id; stamped on the trace span.
variablesdictnoFrom prompts.render().variables. Recorded on the span, never sent to a BYO provider. Send it whenever prompt_version_id is set — without it the run cannot seed a dataset. With no prompt_version_id, a gateway call renders {{ placeholders }} in your messages with these instead.
tracebool | {trace_id?, session_id?}noDefault True on the BYO path, False on the gateway path.

Returns ChatResult (id, model, content, message, finish_reason, usage, gateway) or, when streaming via gateway.stream(), an async iterator of ChatChunk. gateway carries request_id, provider, model, cost_usd, cache, trace_id, and span_ref read from the gateway's x-gateway-* headers.

note

response_format and tools/tool_choice/tool_refs cannot ride the same gateway request — the gateway returns a 400. To get a typed answer that also calls tools, pass both to gateway.run_tool_loop; the SDK gathers with tools, then makes one shaping call with the format.

gateway.run_tool_loop(...)

The full agent loop: calls the model, runs the tools it asks for, appends the results, and repeats until the model stops calling tools or max_iterations is hit. Tools requested in one turn run concurrently.

from acruxcore import acrux

@acrux.tool
async def get_weather(city: str) -> dict:
"""Get the current weather for a city.

Args:
city: City name, e.g. 'Lahore'.
"""
return {"tempC": 21}

result = await hub.gateway.run_tool_loop(
"agent-model",
[{"role": "user", "content": "Weather in Lahore?"}],
tools=[get_weather],
)
print(result.content, result.trace_id)
ParameterTypeRequiredNotes
modelstryesModel public name.
messageslist[dict]yesSeed messages.
toolslist[Callable]noFunctions decorated with @acrux.tool. Run locally; reconciled with the catalog.
tool_defslist[ToolDefinition]noRaw OpenAI definitions, sent inline; route to dispatch.
tool_refslist[{name, alias?}]noCatalog refs. http executor runs on the platform.
client_toolsdict[str, Callable]noCatalog tool name → the function that runs it, for client executors. Writes nothing to the catalog; keeps the binding's alias or pin. Called with the schema's own field names as keywords.
dispatchCallable[[str, dict], Any]no(name, args) -> result (sync or async). Required for tool_defs, and the fallback for a client ref with no decorated tool and no client_tools entry.
syncboolnoReconcile tools with the catalog first. Default True.
max_iterationsintnoMax round-trips. Default 10.
temperaturefloatnoSampling temperature.
max_tokensintnoMax completion tokens.
response_formatResponseFormatnoShapes the final answer; may be combined with tools (gather + shape).
tracebool | {trace_id?, name?, session_id?}noDefault True.
providerProviderConfignoPer-call BYO override.
prompt_version_idstrnoStamped on every llm span this loop records.
variablesdictnoStamped alongside it. Same argument as on chat().

Returns RunToolLoopResult(content, messages, iterations, stopped_at_limit, trace_id). Raises MISSING_DISPATCH before the first model call if a tool has no runner — the message names the tool and, when client_tools was passed, lists the keys it held. Raises VALIDATION_ERROR, also before the first call, when a client_tools function cannot receive its tool's required arguments.

gateway.run_prompt_with_tools(rendered, ...)

The same loop, with everything it needs taken from a render result: the version's bound model, the rendered messages, the tools bound to this prompt alias, and the version id that stamps trace lineage. Your code adds the user's turn and the tools it runs itself.

rendered = await hub.prompts.render("travel-planner", "production", {"today": "2026-08-21"})
messages = [*rendered.messages, {"role": "user", "content": question}]

result = await hub.gateway.run_prompt_with_tools(
rendered,
messages=messages,
client_tools={"search_flights": search_flights},
)
ParameterTypeRequiredNotes
renderedRenderResultyesFrom prompts.render. Positional.
modelstrnoOverrides the version's bound model.
messageslist[dict]noOverrides the rendered messages.
tool_refslist[{name, alias?}]noOverrides the prompt's bindings entirely. [] runs the prompt with no tools.
client_toolsdict[str, Callable]noThe prompt's client-executor tools, keyed by name. Its http tools need no entry — the platform runs them.
streamboolnoTrue returns the same event stream as run_tool_loop.

Every other keyword of run_tool_loop is accepted and passes straight through. A prompt with no tools bound still runs, as a plain completion.

Returns the same RunToolLoopResult, or an event stream when stream=True. Raises VALIDATION_ERROR when the version has no bound model and no model= was passed.

traces.ingest(input, *, wait=True)

Report a trace (a group of spans) to AcruxCore. A single-trace convenience over the batch endpoint — omit traceId to mint a new trace, pass one to append.

result = await hub.traces.ingest({
"name": "rag-pipeline",
"spans": [
{"spanId": "retrieval-1", "name": "vector-search", "kind": "retrieval",
"status": "ok", "startTime": "2026-01-01T00:00:00Z",
"endTime": "2026-01-01T00:00:01Z",
"input": {"query": "shipping policy"}, "output": {"hits": 4}},
],
})
print(result.trace_id)
FieldTypeRequiredNotes
traceIdstrnoOmit to mint a new trace; pass one to append.
sessionIdstrnoGroups traces into a session.
namestrnoTrace name.
capturePayloadsboolnoForce payload capture on for this trace.
tagslist[str]noSet on creation; merged (union) on append.
metadatadictnoSet on creation; shallow-merged on append.
spanslist[IngestSpan]yesThe spans to report — see Span shapes.

Returns TraceResult(trace_id).

Reporting without waiting

wait=True (the default) awaits the POST: errors are raised at the call site and trace_id is the server's. wait=False buffers the trace and returns immediately, so instrumenting a retrieval or rerank step costs no round trip:

result = await hub.traces.ingest({"name": "retrieval", "spans": [...]}, wait=False)

# Usable straight away — the id is generated client-side, and the API creates
# the trace under it. Hand it to the gateway so both land on one trace.
await hub.gateway.chat(model="gpt-4o-mini", messages=[...], trace={"traceId": result.trace_id})

await hub.traces.flush() # before reading it back, or before the process exits

The trade: nothing is confirmed at the call site. A failed send warns once per error kind and drops the batch, the same way the gateway's own span reporting already behaves. traces.flush() waits for everything buffered — including the gateway's spans, which share the buffer — and AcruxCore.aclose() flushes too.

traces.flush()

Wait for every trace buffered by ingest(..., wait=False) (and by the gateway's own span reporting) to be sent. Returns once the queue is empty.

traces.submit_feedback(...) / traces.update_feedback(...)

Attach feedback to a trace (or one span), then edit it in place. Only the original author may edit.

fb = await hub.traces.submit_feedback(trace_id, rating=5, label="helpful", comment="Resolved my issue.")
await hub.traces.update_feedback(trace_id, fb.id, rating=1, label="unhelpful")

submit_feedback(trace_id, *, ...) — at least one of rating/label/comment:

FieldTypeNotes
trace_idstrRequired (positional).
span_idstrScope to one span.
ratingint-1..5.
labelstrShort label.
commentstrFree text.
sourcestr"user" | "developer" | "end_user" | "api".

update_feedback(trace_id, feedback_id, *, rating=..., label=..., comment=...) — pass a value to change it, None to clear it, or omit the argument to keep the existing value (uses an Ellipsis sentinel internally).

Returns FeedbackResult (id, trace_id, span_id, rating, label, comment, source, created_by, created_at, updated_at).

traces.get(trace_id) / traces.list(...)

Read traces back. traces.get returns the header plus the full span tree; traces.list returns one page of summaries, newest first.

full = await hub.traces.get(trace_id)
print(full.trace, full.spans)

page = await hub.traces.list(status="error", min_latency_ms=2000, limit=20)
print(page.data, page.total)

traces.list(*, ...) filters — all keyword-only and optional:

FieldTypeNotes
from_ / tostrISO date range (from_ maps to from).
statusstr'ok' | 'error' | 'unset'.
modelstrFilter by model.
session_idstrFilter by session.
prompt_version_idstrFilter by prompt version.
min_latency_msintMinimum latency.
min_cost_usdfloatMinimum cost.
min_tokensintMinimum total tokens.
qstrFree-text search.
page / limitintPagination (1-based page).

traces.get returns GetTraceResult(trace, spans); traces.list returns ListTracesResult(data, total, page, limit).

Trace analytics (hub.traces)

traces.analytics(*, group_by=None, since=None, until=None)

Aggregated trace metrics (latency, cost, tokens) grouped by a facet.

analytics = await hub.traces.analytics(group_by="model")
ParameterTypeNotes
group_bystrFacet key to group by (e.g. "model", "status").
since / untilstrISO-8601 time bounds.

Returns AnalyticsResult (data).

traces.list_facets()

Discover available facet keys for grouping.

facets = await hub.traces.list_facets()

Returns TraceFacets — object whose keys are facet names.

traces.get_facet_values(key)

Get distinct values for a facet key.

values = await hub.traces.get_facet_values("model")

Returns FacetValuesResult (values).

traces.get_settings() / traces.update_settings(capture_payloads)

Read or update the team's trace capture settings.

settings = await hub.traces.get_settings()
await hub.traces.update_settings(True) # enable payload capture

Returns TraceSettings (capture_payloads).

traces.get_feedback_summary(*, since=None, until=None)

Aggregated feedback buckets (rating distribution).

summary = await hub.traces.get_feedback_summary()

Returns FeedbackSummaryResult (data).

traces.list_feedback(*, limit=None, page=None)

Paginated feedback list across all traces.

feedback = await hub.traces.list_feedback(limit=10)

Returns FeedbackListResult (data, total, page, limit).

traces.get_trace_feedback(trace_id)

All feedback for a specific trace.

fb = await hub.traces.get_trace_feedback(trace_id)

Returns TraceFeedbackResult (data).

gateway.flush() / gateway.aclose() / aclose()

await hub.gateway.flush() # wait for background trace writes to finish
await hub.gateway.aclose() # flush, then stop accepting spans
await hub.aclose() # the above, then close the HTTP connection pool

gateway.chat(), streaming, and gateway.run_tool_loop() hand back their result without waiting for the trace write — call gateway.flush() before reading the traces API back. A script that returns from main() does not need either: the SDK drains at interpreter exit.

hub.aclose() is the full shutdown, and it is what async with AcruxCore() as hub: calls on exit. Reach for it directly wherever async with is not available — a notebook cell, a REPL, or a server's own shutdown hook. gateway.aclose() stops at the span queue and leaves the connection pool open, so code that builds a client per request or per job should call hub.aclose() instead.

Tool catalog (hub.tools)

Catalog operations live on hub.tools.

tools.sync(...) / tools.sync_one(...)

Reconcile tools from @acrux.tool against the catalog. Idempotent and cached per process on the spec hash — a second call with an unchanged tool makes no request.

await hub.tools.sync([get_weather])
one = await hub.tools.sync_one(get_weather.__acrux_tool__, on_conflict="error")
ParameterTypeNotes
tools / speclist[Callable] / ToolSpecDecorated functions (for sync) or a ToolSpec (for sync_one).
on_conflictstrDefault "warn". "error" raises when a commit supersedes a dashboard-authored version.

Returns a list of ToolSyncResult (tool_id, version_number, committed, alias, superseded_source) from sync, or a single one from sync_one. committed is False on a cache hit.

tools.resolve(refs)

Resolve catalog refs to schemas plus executor types in one request.

[resolved] = await hub.tools.resolve([{"name": "get_weather", "alias": "production"}])
# resolved.tool_id, resolved.executor_type ('client' | 'http'), resolved.function

Returns list[ResolvedTool] (tool_id, version_number, executor_type, function). Raises API_ERROR (404) when any ref does not resolve.

tools.execute(tool_id, args, *, ...)

Run a tool's server-side http executor on the platform. The platform writes the tool span itself — do not report one for the same call.

out = await hub.tools.execute(
tool_id, {"city": "Lahore"},
alias="production", trace_id=trace_id, parent_span_id=llm_span_ref,
)
print(out.result, out.latency_ms, out.tool_version_id)
ParameterTypeNotes
tool_idstrFrom resolve(). Required (positional).
argsdictThe model's parsed arguments. Required (positional).
aliasstrWhich alias to run.
version_numberintPin an exact version.
trace_idstrAttach the span to this trace.
parent_span_idstrNest under this span (normally the llm span).

Returns ToolExecuteResult(result, status, latency_ms, tool_version_id).

tools.list(*, search=None, page=None, limit=None)

List tools for the team, newest first.

page = await hub.tools.list(search="weather", limit=10)
ParameterTypeNotes
searchstrFree-text search on name.
pageint1-based page.
limitintPage size.

Returns ToolListResult(data, total, page, limit).

tools.get(tool_id)

Fetch one tool's shell by id.

tool = await hub.tools.get(tool_id)

Returns ToolDetail (id, name, description, latest_version, created_at, updated_at).

tools.create(name, *, description=None)

Create a new tool shell. Commit a version with commit_version to give it a schema/executor.

tool = await hub.tools.create("get_weather", description="Weather lookup")

Returns ToolDetail.

tools.update(tool_id, **kwargs)

Update a tool's name and/or description. Does not touch versions.

updated = await hub.tools.update(tool_id, description="Updated description")

Returns ToolDetail.

tools.delete(tool_id)

Soft-delete a tool. Versions and aliases are preserved but unreachable. Returns None.

await hub.tools.delete(tool_id)

tools.commit_version(tool_id, *, description=None, parameters_schema, executor, changelog=None)

Commit a new immutable version for a tool. The first commit auto-creates production and staging aliases.

version = await hub.tools.commit_version(
tool_id,
description="Get weather for a city",
parameters_schema={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
executor={"type": "client"},
)

Returns ToolVersionDetail.

tools.list_versions(tool_id, *, page=None, limit=None)

List a tool's versions, newest first. Items omit parameters_schema/executor.

versions = await hub.tools.list_versions(tool_id)

Returns ToolVersionListResult(data, total, page, limit).

tools.get_version(tool_id, version_number)

Fetch one version with its full parameters_schema/executor.

v = await hub.tools.get_version(tool_id, 1)

Returns ToolVersionDetail.

tools.promote_alias(tool_id, alias, version_number)

Promote an alias to point at a specific version. Creates the alias if it doesn't exist.

alias = await hub.tools.promote_alias(tool_id, "production", 2)

Returns ToolAliasDetail (alias, version_number, tool_id).

tools.analytics(*, since=None, until=None)

Read aggregated call analytics (count, error rate, p50/p95 latency) per tool.

analytics = await hub.tools.analytics(since="2026-01-01T00:00:00Z")

Returns ToolAnalyticsResult (data).

@acrux.tool

Mark a function as a tool, deriving its name, description, and parameter schema from the function itself. Works bare (@acrux.tool) and called (@acrux.tool(alias="staging")). The function stays directly callable.

from acruxcore import acrux

@acrux.tool
async def get_weather(city: str) -> dict:
"""Get the current weather for a city.

Args:
city: City name, e.g. 'Lahore'.
"""
return {"tempC": 21}

The decorator is pure — it performs no network calls and has no import-time side effects. The schema is derived from the signature; supported hints are str, int, float, bool, list[T], dict, Optional[T], Literal[...], and Enum subclasses. A parameter without a default is required, and each parameter's description comes from the docstring's Google-style Args: block.

ParameterTypeRequiredNotes
fnCallable(bare use)The function, when used bare. Never pass explicitly.
namestrnoDefaults to the function's name.
descriptionstrnoDefaults to the docstring's first paragraph.
parametersdictnoHand-written JSON Schema; skips derivation. The escape hatch for a type this converter cannot model.
aliasstrnoCatalog alias a sync moves. Default "production".
changelogstrnoRelease note for humans; never shown to the model.

A function with no docstring sends no description, which hands ownership of the model-facing text to the dashboard. Raises ToolSchemaError at decoration time when the schema cannot be derived (a missing hint, an unsupported type, or *args/**kwargs).

Structured output

response_format asks the model for a typed answer. Pass an OpenAI-shaped dict, or build one from a pydantic v2 model with pydantic_response_format — field-level Field(description=...) guidance reaches the model without hand-writing JSON Schema.

from pydantic import BaseModel, Field
from acruxcore import pydantic_response_format

class Sentiment(BaseModel):
"""Classify the message."""
sentiment: str = Field(description="'pos', 'neg', or 'neutral'")

await hub.gateway.chat(
"agent-model", messages,
response_format=pydantic_response_format(Sentiment, name="sentiment"),
)

Pydantic is an optional dependency — pydantic_response_format() does not import it; the SDK resolves the schema at send time. The available shapes:

VariantShape
text{"type": "text"}
json_object{"type": "json_object"}
json_schema{"type": "json_schema", "json_schema": {"name": ..., "schema": ..., "strict": ...}}
pydanticpydantic_response_format(Model, *, name, strict=True)

The gateway forwards the format to each provider's native structured-output mode and relies on the provider to honour it — it does not validate the returned content against the schema, so parse and validate on your side when conformance matters. Raises PYDANTIC_NOT_AVAILABLE if a pydantic-built format is sent but pydantic is not installed.

BYO provider

Route a call directly to your own OpenAI-compatible endpoint instead of the gateway — the hop and its latency are skipped, and api_key is sent only to base_url, never to AcruxCore.

hub = AcruxCore(
provider={"base_url": "https://api.groq.com/openai/v1", "api_key": os.environ["GROQ_API_KEY"]},
)

Pass provider to the constructor (a default for every call) or per-call to gateway.chat()/gateway.run_tool_loop(). There is no server-side catalog on this path, so every tool is sent inline as a full schema, and the SDK reports one llm span per round-trip. gateway.cost_usd and gateway.cache are always None (the gateway never saw the call). Raises PROVIDER_ERROR for a non-2xx provider response.

Span shapes

IngestSpan (the dict shape passed in spans to traces.ingest()):

FieldTypeNotes
spanIdstrRequired; opaque, unique within the trace.
parentSpanIdstrLinks to another span's spanId.
namestrRequired.
kindstr'llm' | 'tool' | 'retrieval' | 'embedding' | 'agent' | 'chain' | 'other'.
statusstr'ok' | 'error' | 'unset'.
startTimestrRequired; ISO-8601 with offset.
endTimestrISO-8601.
model, providerstrModel/provider metadata.
usagedict{promptTokens?, completionTokens?, totalTokens?}.
costUsdfloatCost in USD.
promptVersionIdstrPrompt lineage.
variablesAnyThe prompt variables behind input. Read back when feedback becomes a dataset example.
input / outputanyStored only with payload capture on.
attributesdictFree-form.
errorstrError message for failed spans.

Error codes

All failures raise AcruxCoreError with a code attribute — use it for programmatic handling rather than matching on the message.

CodeWhen it is raised
MISSING_API_KEYNo api_key in args or env; or a BYO provider["api_key"] is empty.
MISSING_BASE_URLNo base_url in args or env; or a BYO provider["base_url"] is empty.
NETWORK_ERRORAll retries exhausted at the network level.
API_ERRORNon-retryable HTTP error from the gateway (4xx, or 5xx after retries). Inspect status_code/body.
MISSING_VARIABLESTemplate requires variables you did not supply.
TOOL_SCHEMA_ERROR@acrux.tool: a missing hint, an unsupported type, or *args/**kwargs.
MISSING_DISPATCHrun_tool_loop: a tool has no implementation (raised before the first model call).
PYDANTIC_NOT_AVAILABLEA pydantic-built response_format was sent but pydantic is not installed.
PROVIDER_ERRORBYO: non-2xx response from your provider endpoint.

Sessions (hub.sessions)

sessions.list(*, page=None, limit=None)

List sessions with pagination.

page = await hub.sessions.list(limit=10)
ParameterTypeNotes
pageint1-based page.
limitintPage size.

Returns SessionListResult(data, total, page, limit).

sessions.get(session_id)

Get session detail with all traces.

session = await hub.sessions.get(session_id)

Returns SessionDetailResult (session, traces).

Evaluations

Datasets (hub.datasets)

MethodDescription
datasets.create(name, *, overall_feedback=None)Create a dataset. Returns DatasetDto.
datasets.build_from_feedback(name, feedback_ids, *, overall_feedback=None)Build a dataset from trace feedback. Returns BuildFromFeedbackResult.
datasets.list()List all datasets. Returns list[DatasetDto].
datasets.get(dataset_id)Get dataset with examples. Returns DatasetWithExamples.
datasets.update(dataset_id, **kwargs)Update dataset metadata (name, overall_feedback). Returns DatasetDto.
datasets.delete(dataset_id)Delete a dataset. Returns None.
datasets.add_example(dataset_id, input, *, criteria=None, history=None)Add an example. Returns DatasetExampleDto.
datasets.remove_example(dataset_id, example_id)Remove an example. Returns None.

Experiments (hub.experiments)

MethodDescription
experiments.create(dataset_id, version_ids, models, *, prompt_id=None, name=None, alias=None)Create an experiment. Returns ExperimentDto.
experiments.list()List all experiments. Returns list[ExperimentDto].
experiments.get(experiment_id)Get an experiment. Returns ExperimentDto.
experiments.start_run(experiment_id)Start a run. Returns StartRunResult (run_id, status).

Runs (hub.runs)

MethodDescription
runs.list(*, status=None, dataset_id=None, prompt_id=None, page=None, limit=None)List runs. Returns RunListResponse.
runs.get(run_id)Get run detail. Returns RunDetailDto.
runs.get_report(run_id)Get run report. Returns RunReport.
runs.get_cell(run_id, cell_key)Get a specific cell. Returns RunCellDetailDto.
runs.get_candidate(run_id, candidate_id)Get a candidate. Returns CandidateDetail.
runs.promote_candidate(run_id, prompt_candidate_id, *, alias=None)Promote a candidate. Returns PromoteResult.

Optimize (hub.optimize)

MethodDescription
optimize.start(prompt_id, dataset_id, models, *, draft_count=None, alias=None)Start prompt optimization. Returns StartOptimizeResult (run_id, status, prompt_mismatch_warning).

Where to next