Node SDK reference
@acruxcoreai/sdk is the Node/TypeScript 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 Python counterpart in
acruxcore — only the casing and a few option-object shapes differ.
npm install @acruxcoreai/sdk
Construct the client
import AcruxCore from '@acruxcoreai/sdk';
// Reads ACRUXCORE_API_KEY and ACRUXCORE_BASE_URL from the environment.
const hub = new AcruxCore();
Create one instance at process startup and reuse it. The render cache is a module-level singleton sized by the first constructor.
| Option | Type | Default | Notes |
|---|---|---|---|
apiKey | string | process.env.ACRUXCORE_API_KEY | Throws MISSING_API_KEY if neither is set. |
baseUrl | string | process.env.ACRUXCORE_BASE_URL | Throws MISSING_BASE_URL if neither is set. |
cacheTtl | number (ms) | 60000 | Render cache freshness window. 0 disables caching (and serve-stale). |
maxCacheSize | number | 500 | Max LRU entries. Set by the first constructor. |
maxRetries | number | 1 | Retries this SDK's own request to AcruxCore on a transient failure (2 total attempts). It does not change how many times a provider is called — that is the per-call gateway.maxRetries. |
retryInterval | number (ms) | 500 | Delay between retries. |
provider | ProviderConfig | — | Client-level BYO default; overridden by a per-call provider. |
Errors throw an acruxcoreError with a machine-readable code — see
Error codes.
prompts.render(name, alias, variables?)
Render a stored prompt by name + alias into templated messages, plus the tools
attached to that version. Cached per (apiKey, 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.
const { messages, tools, model, versionId } = await hub.prompts.render(
'support-reply',
'production',
{ company: 'Acme', customer_message: 'Order #123 is late' },
);
| Parameter | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Prompt slug (not id). |
alias | string | yes | e.g. 'production', 'staging'. |
variables | Record<string, unknown> | no | Template variables. Defaults to {}. |
Returns { messages, tools, model, versionId, versionNumber, variables }. model is
the version's bound default (or null); variables echoes back what you called with.
Pass versionId as promptVersionId and variables as variables to
gateway.chat()/gateway.runToolLoop() — the pair is what gives a trace prompt lineage
and lets feedback on it become an evaluation dataset example.
gateway.runPromptWithTools(rendered) does both for you. Throws MISSING_VARIABLES if
the template needs a variable you did not supply.
Prompt lifecycle (hub.prompts)
prompts.list(options?)
List prompts for the team, newest first.
const page = await hub.prompts.list({ search: 'support', limit: 10 });
| Parameter | Type | Notes |
|---|---|---|
options.search | string | Free-text search on name. |
options.page | number | 1-based page. |
options.limit | number | Page size. |
Returns { data: PromptSummary[], total, page, limit }.
prompts.get(id)
Fetch one prompt by id.
const prompt = await hub.prompts.get(promptId);
Returns PromptDetail ({ id, name, description, versionCount, createdAt, updatedAt }).
prompts.create(input)
Create a new prompt shell. Commit a version with commitVersion to give it content.
const prompt = await hub.prompts.create({ name: 'support-bot', description: 'Customer support' });
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Unique per team. |
description | string | no | Human-readable. |
Returns PromptDetail.
prompts.update(id, input)
Update a prompt's name and/or description. Does not touch versions.
const updated = await hub.prompts.update(promptId, { description: 'v2 description' });
Returns PromptDetail.
prompts.delete(id)
Delete a prompt and every version/alias under it. Returns void.
await hub.prompts.delete(promptId);
prompts.commitVersion(promptId, input)
Commit a new immutable version for a prompt. The first commit auto-creates production and staging aliases.
const version = await hub.prompts.commitVersion(promptId, {
messages: [
{ role: 'system', content: 'You are a support agent.' },
{ role: 'user', content: '{{question}}' },
],
model: 'gpt-4o-mini',
});
| Field | Type | Required | Notes |
|---|---|---|---|
messages | Message[] | yes | Chat messages (may contain {{variables}}). |
model | string | no | Default model for this version. |
tools | { toolId: string }[] | no | Attach catalog tools (max 64). |
Returns VersionDetail ({ id, versionNumber, messages, model, tools?, aliases? }).
aliases is present only on the first version.
prompts.listVersions(promptId, options?)
List a prompt's versions, newest first. Items omit messages — use getVersion for full content.
const versions = await hub.prompts.listVersions(promptId);
Returns { data: VersionSummary[], total, page, limit }.
prompts.getVersion(promptId, versionNumber)
Fetch one version with its full messages.
const v = await hub.prompts.getVersion(promptId, 1);
Returns VersionDetail.
prompts.getByAlias(name, alias)
The version an alias points at, found by prompt name. Its messages are the
template, with {{ placeholders }} still in them. Use it when your app renders
templates itself; use prompts.render
when the server should fill them in. Pass version.id as promptVersionId to
link the model call to this version.
const version = await hub.prompts.getByAlias('support-reply', 'production');
const messages = myRender(version.messages, { ticket });
const reply = await hub.gateway.chat({ model: 'support-model', messages, promptVersionId: version.id });
Returns VersionDetail, a fresh copy on every call, so changing its messages
in place is safe. Cached for cacheTtl per (name, alias), up to maxCacheSize
entries; a miss makes one request (three against a server older than GET /prompt-versions). Throws API_ERROR with statusCode 404
when no prompt has that exact name, or the prompt has no such alias. Not in 0.17.0;
comes in the next release.
prompts.diff(promptId, from, to)
Diff two versions.
const diff = await hub.prompts.diff(promptId, 1, 2);
Returns DiffResult ({ changes: ChangeEntry[] }).
prompts.promoteAlias(promptId, alias, versionNumber)
Point an alias (e.g. 'production') at a specific version. Creates the alias if it doesn't exist.
const alias = await hub.prompts.promoteAlias(promptId, 'production', 3);
Returns AliasDetail ({ alias, versionNumber, promptId }).
prompts.listAliases(promptId)
Read every alias on a prompt and the version each one points at — the read half of
promoteAlias, for a deploy check or a CI guard asking "which version is production
on right now?".
for (const a of await hub.prompts.listAliases(promptId)) {
console.log(a.alias, '->', `v${a.versionNumber}`);
}
Returns AliasDetail[]. Empty for a prompt with no committed version, since the
first version is what mints the aliases. Added in 0.10.0.
prompts.exportVersion(promptId, versionNumber)
Export a version for portability (JSON blob).
const exported = await hub.prompts.exportVersion(promptId, 1);
Returns ExportedPromptVersion.
prompts.importPrompt(exportData)
Import an exported prompt as a new prompt with one version.
const imported = await hub.prompts.importPrompt(exported);
Returns ImportPromptResult ({ promptId, promptName, versionNumber }).
prompts.tracesForVersion(promptId, versionNumber, options?)
List traces that used a specific prompt version.
const traces = await hub.prompts.tracesForVersion(promptId, 1, { limit: 10 });
Returns { data: TraceSummary[], total, page, limit }.
Which call runs your tools
| You have | Call |
|---|---|
| Messages you wrote, no tools | gateway.chat |
| Messages you wrote, and the tools named on the call | gateway.runToolLoop |
| A prompt from the catalog | prompts.render, then gateway.runPromptWithTools |
chat() takes tools too, but only offers them: it returns with
finishReason: 'tool_calls' and the request on message.tool_calls, and running it is
yours to do. It also rejects a tool declared with acrux.tool — that shape belongs to
runToolLoop, and the error says so.
Inside the loop, what you pass depends on where the definition lives and whose process runs the call:
| Definition lives in | Runs in | Pass |
|---|---|---|
Your code, declared with acrux.tool | Your process | tools: [getWeather] |
The catalog, http executor | AcruxCore | Nothing — the prompt's binding is enough, or toolRefs: [{ name }] |
The catalog, client executor | Your process | clientTools: { get_weather: getWeather } |
| Nowhere — inline for this one call | Your process | toolDefs: [...] and dispatch |
dispatch is the most general option. Use dispatch when the tool names are not
known until runtime.
Call a prompt's tools from the SDK works through each shape end to end, streaming included.
gateway.chat(options)
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.runToolLoop to dispatch
them.
const { content, usage, gateway } = await hub.gateway.chat({
model: 'support-model',
messages,
temperature: 0.2,
});
For streaming, use gateway.stream() which returns an async iterable of chunks:
const stream = await hub.gateway.stream({ model: 'support-model', messages });
for await (const chunk of stream) process.stdout.write(chunk.delta.content ?? '');
Leaving the loop early with break closes the connection to the model at once. A BYO stream left
this way still records its llm span, with the text received so far and the attribute
clientClosed: true. Token usage is usually missing on that span, since the provider sends it last.
| Parameter | Type | Required | Notes |
|---|---|---|---|
model | string | yes | Model public name. |
messages | Message[] | yes | Chat messages. |
tools | ToolDefinition[] | no | Inline OpenAI-shaped tool definitions. |
toolRefs | { name; alias? }[] | no | Catalog tool references (resolved server-side). |
toolChoice | 'auto' | 'none' | 'required' | { type:'function'; function:{name} } | no | How the model uses tools. |
responseFormat | ResponseFormat | no | Structured output. Mutually exclusive with tools/toolChoice. |
temperature | number | no | Sampling temperature. |
maxTokens | number | no | Max completion tokens. |
reasoningEffort | string | no | How hard a reasoning model thinks, e.g. 'low' or 'high'. Sent as reasoning_effort. Not in 0.17.0; comes in the next release. |
stream | boolean | no | Return an async iterable of ChatChunk instead of ChatResult. |
extraBody | Record<string, unknown> | no | More OpenAI fields (top_p, seed, stop, …) added to the body on both paths. A key another option already sets, or model / messages / stream, throws VALIDATION_ERROR before anything is sent. The gateway forwards only fields it knows and names the rest in x-gateway-params-ignored. Not in 0.17.0; comes in the next release. |
provider | ProviderConfig | no | Per-call BYO override. |
gateway | { maxRetries?: number; fallback?: boolean } | no | Per-call gateway controls: how many times the provider may be retried (0–5), and whether a failure may fall back to the next model in the chain. A different layer from the client's maxRetries, which only retries this SDK's request to AcruxCore. Ignored on a BYO call. |
promptVersionId | string | no | From prompts.render().versionId; stamped on the trace span. |
variables | Record<string, unknown> | no | From prompts.render().variables. Recorded on the span, never sent to a BYO provider. Send it whenever promptVersionId is set — without it the run cannot seed a dataset. With no promptVersionId, a gateway call renders {{ placeholders }} in your messages with these instead. |
trace | boolean | { traceId?; parentSpanId?; name?; sessionId?; tags?; metadata? } | no | Default true on the BYO path, false on the gateway path. parentSpanId puts the call's llm span under a span you reported. name names a new trace on both paths. Inside traces.step the call joins the step on its own. |
Returns ChatResult ({ id, model, content, message, finishReason, usage?, gateway })
or, when streaming via gateway.stream(), an AsyncGenerator<ChatChunk>. gateway carries
requestId, provider, model, costUsd, cache, traceId, and spanRef
read from the gateway's x-gateway-* headers. usage also carries cachedTokens (part of
promptTokens) and reasoningTokens (part of completionTokens) when the provider reports them.
responseFormat and tools/toolChoice/toolRefs cannot ride the same gateway
request — the gateway returns a 400. To get a typed answer that also calls tools,
pass both to gateway.runToolLoop; the SDK gathers with tools,
then makes one shaping call with the format.
gateway.runToolLoop(options)
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 maxIterations is
hit. Tools requested in one turn run concurrently.
import { acrux } from '@acruxcoreai/sdk';
const getWeather = acrux.tool(
{ name: 'get_weather', parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] } },
async ({ city }) => ({ tempC: 21 }),
);
const { content, traceId } = await hub.gateway.runToolLoop({
model: 'agent-model',
messages: [{ role: 'user', content: 'Weather in Lahore?' }],
tools: [getWeather],
});
| Parameter | Type | Required | Notes |
|---|---|---|---|
model | string | yes | Model public name. |
messages | Message[] | yes | Seed messages. |
tools | AcruxTool[] | no | Tools from acrux.tool. Run locally; reconciled with the catalog. |
toolDefs | ToolDefinition[] | no | Raw OpenAI definitions, sent inline; route to dispatch. |
toolRefs | { name; alias? }[] | no | Catalog refs. http executor runs on the platform. |
clientTools | Record<string, (args) => unknown> | no | Catalog tool name → the function that runs it, for client executors. Writes nothing to the catalog; keeps the binding's alias or pin. Called with one arguments object. |
dispatch | (name, args) => unknown | Promise<unknown> | no | Fallback runner. Required for toolDefs, and the fallback for a client ref with no matching tools or clientTools entry. |
sync | boolean | no | Reconcile tools with the catalog first. Default true. |
maxIterations | number | no | Max round-trips. Default 10. |
temperature | number | no | Sampling temperature. |
maxTokens | number | no | Max completion tokens. |
reasoningEffort | string | no | How hard a reasoning model thinks, e.g. 'low' or 'high'. Sent as reasoning_effort. Not in 0.17.0; comes in the next release. |
responseFormat | ResponseFormat | no | Shapes the final answer; may be combined with tools (gather + shape). |
trace | boolean | { traceId?; name?; sessionId? } | no | Default true. |
provider | ProviderConfig | no | Per-call BYO override. |
gateway | { maxRetries?: number; fallback?: boolean } | no | Per-call gateway controls: how many times the provider may be retried (0–5), and whether a failure may fall back to the next model in the chain. A different layer from the client's maxRetries, which only retries this SDK's request to AcruxCore. Ignored on a BYO call, and applied to every round of the loop. |
promptVersionId | string | no | Stamped on every llm span this loop records. |
variables | Record<string, unknown> | no | Stamped alongside it. Same field as on chat(). |
Returns RunToolLoopResult ({ content, messages, iterations, stoppedAtLimit, traceId? }).
Throws MISSING_DISPATCH before the first model call if a tool has no runner — the
message names the tool and, when clientTools was passed, lists the keys it held.
gateway.runPromptWithTools(rendered, options?)
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.
const rendered = await hub.prompts.render('travel-planner', 'production', { today });
const messages = [...rendered.messages, { role: 'user', content: question }];
const result = await hub.gateway.runPromptWithTools(rendered, {
messages,
clientTools: { search_flights: searchFlights },
});
| Parameter | Type | Required | Notes |
|---|---|---|---|
rendered | RenderResult | yes | From prompts.render. Positional. |
model | string | no | Overrides the version's bound model. |
messages | Message[] | no | Overrides the rendered messages. |
toolRefs | { name; alias? }[] | no | Overrides the prompt's bindings entirely. [] runs the prompt with no tools. |
clientTools | Record<string, (args) => unknown> | no | The prompt's client-executor tools, keyed by name. Its http tools need no entry — the platform runs them. |
stream | boolean | no | true returns the same event stream as runToolLoop. |
Every other option of runToolLoop 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.
Throws VALIDATION_ERROR when the version has no bound model and no model was passed.
traces.ingest(input, options?)
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.
const { traceId } = 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 } },
],
});
| Parameter | Type | Required | Notes |
|---|---|---|---|
traceId | string | no | Omit to mint a new trace; pass one to append. |
sessionId | string | no | Groups traces into a session. |
name | string | no | Trace name. |
capturePayloads | boolean | no | Force payload capture on for this trace. |
tags | string[] | no | Set on creation; merged (union) on append. |
metadata | Record<string, unknown> | no | Set on creation; shallow-merged on append. |
spans | IngestSpan[] | yes | The spans to report. |
Returns { traceId }.
Reporting without waiting
wait: true (the default) awaits the POST: errors throw at the call site and
traceId is the server's. { wait: false } buffers the trace and returns
immediately, so instrumenting a retrieval or rerank step costs no round trip:
const { traceId } = 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 } });
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 gateway.close() flushes too.
traces.flush()
Wait for every trace buffered by ingest(…, { wait: false }) (and by the
gateway's own span reporting) to be sent. Resolves once the queue is empty.
traces.step(name, options?, fn)
Run one step of your own code — a retrieval, a parse, a whole request handler —
as a span. fn runs at once and gets the open step; step() returns what fn
returns. traces.wrap does the same for every call
of a function.
const classify = hub.traces.wrap('classify', { kind: 'chain' }, async (ticket: string) => {
const reply = await hub.gateway.chat({ model: 'support-model', messages: [{ role: 'user', content: ticket }] });
return reply.content;
});
const answer = await hub.traces.step('handle-ticket', { kind: 'agent' }, () => classify(ticket));
This gives one trace named handle-ticket, with classify under it and the
model call under classify:
handle-ticket agent
└─ classify chain
└─ llm support-model
A step opened inside another step is its child. A gateway.chat() or
gateway.stream() inside a step joins the step's trace, with its llm span
under the step. This works on the gateway path and the BYO path.
runToolLoop does not join on its own; pass it
trace: { traceId: step.traceId }, and the trace still takes the step's name.
| Option | Type | Default | Notes |
|---|---|---|---|
kind | SpanKind | 'chain' | llm, tool, retrieval, embedding, agent, chain or other. |
input | unknown | none (wrap: the arguments) | wrap() records the call's arguments as an array. |
captureInput / captureOutput | boolean | true | false records no input / output. |
sessionId | string | — | Session for the trace. |
tags / metadata | string[] / object | — | Added to the trace, from any step. |
The return value is the step's output; set step.output to record something
else. step.spanId and step.traceId are readable inside fn. An async fn
keeps the step open until its promise settles. A thrown error or a rejected
promise marks the span status: 'error' and is thrown again unchanged. A value
JSON cannot hold is stored safely: a bigint as its digits, a repeated object
as '[Circular]'.
await hub.traces.step('retrieve', { kind: 'retrieval' }, async (step) => {
const docs = await search(query);
step.output = { hits: docs.length };
});
The whole tree of the outermost step is sent as one report when that step ends,
in the background. So the trace appears once the request finishes, not span by
span. A step that is still running when the outermost step ends — work started
without await — is sent when it ends. Call traces.flush()
before reading a trace back. Not in 0.17.0; comes in the next release.
A step only collects calls made on the client that opened it. A call on a
second client inside the step starts that client's own trace, because the two
clients may belong to different teams. One trace holds at most 1,000 spans.
Spans past that are dropped, and the root span's droppedSpans attribute
counts them.
traces.wrap(name, options?, fn)
Wrap a function so that every call of it is one step, with the same options as
traces.step. The wrapped function takes the same
arguments and returns the same value; this is passed through.
On a generator function (function* or async function*), the step stays open
until the generator ends, and calls made by the code that reads it are not
counted as part of the step. Leaving the loop early with break closes the
generator and ends the step as ok. A generator dropped part-way, without
break or return(), ends its step as ok when it is garbage-collected. A
generator's step records no output.
const retrieve = hub.traces.wrap('retrieve', { kind: 'retrieval' }, async function* (query: string) {
for await (const doc of search(query)) yield doc;
});
Not in 0.17.0; comes in the next release.
traces.submitFeedback(input) / traces.updateFeedback(input)
Attach feedback to a trace (or one span), then edit it in place. Only the original author may edit.
const fb = await hub.traces.submitFeedback({
traceId,
rating: 1,
label: 'helpful',
comment: 'Resolved my issue.',
});
await hub.traces.updateFeedback({ traceId, feedbackId: fb.id, rating: 1, label: 'unhelpful' });
submitFeedback input — at least one of rating/label/comment:
| Field | Type | Notes |
|---|---|---|
traceId | string | Required. |
spanId | string | Scope to one span. |
rating | 1 | -1 | 1 (thumbs up) or -1 (thumbs down). |
label | string | Short label. |
comment | string | Free text. |
source | 'user' | 'developer' | 'end_user' | 'api' | Origin. |
updateFeedback input — { traceId, feedbackId, rating?, label?, comment? }. Pass
null to clear a field; omit it to keep the existing value.
Returns FeedbackResult ({ id, traceId, spanId, rating, label, comment, source, createdBy, createdAt, updatedAt }).
traces.get(traceId) / traces.list(options?)
Read traces back. traces.get returns the header plus the full span tree;
traces.list returns one page of summaries, newest first.
const { trace, spans } = await hub.traces.get(traceId);
const page = await hub.traces.list({ status: 'error', minLatencyMs: 2000, limit: 20 });
traces.list filters — all optional:
| Field | Type | Notes |
|---|---|---|
from / to | string | ISO date range. |
status | SpanStatus | 'ok' | 'error' | 'unset'. |
model | string | Filter by model. |
sessionId | string | Filter by session. |
promptVersionId | string | Filter by prompt version. |
minLatencyMs | number | Minimum latency. |
minCostUsd | number | Minimum cost. |
minTokens | number | Minimum total tokens. |
q | string | Free-text search. |
errorType | SpanErrorType | Traces with a span that failed this way, e.g. 'tool_declared'. Not in 0.19.0; comes in the next release. |
errorCode | string | Traces with a span carrying this declared error code. Not in 0.19.0; comes in the next release. |
hasWarning | boolean | true for traces with a warning, false for none. Not in 0.19.0; comes in the next release. |
tags | string[] | Traces carrying all of these tags. Not in 0.19.0; comes in the next release. |
metadata | Record<string, string> | Traces whose metadata has every one of these pairs. Not in 0.19.0; comes in the next release. |
page / limit | number | Pagination (1-based page). |
traces.get returns { trace: TraceSummary, spans: TraceSpan[] }; traces.list
returns { data: TraceSummary[], total, page, limit }.
Trace analytics (hub.traces)
traces.analytics(options?)
Aggregated trace metrics (latency, cost, tokens) grouped by a facet.
const analytics = await hub.traces.analytics({ group_by: 'model' });
| Parameter | Type | Notes |
|---|---|---|
options.group_by | string | Facet key to group by (e.g. 'model', 'status'). |
options.since / options.until | string | ISO-8601 time bounds. |
Returns AnalyticsResult ({ data: AnalyticsEntry[] }).
traces.listFacets()
Discover available facet keys for grouping.
const facets = await hub.traces.listFacets();
Returns TraceFacets — object whose keys are facet names.
traces.getFacetValues(key)
Get distinct values for a facet key.
const values = await hub.traces.getFacetValues('model');
Returns FacetValuesResult ({ values: string[] }).
traces.getSettings() / traces.updateSettings(capturePayloads)
Read or update the team's trace capture settings.
const settings = await hub.traces.getSettings();
await hub.traces.updateSettings(true); // enable payload capture
Returns TraceSettings ({ capturePayloads: boolean }).
traces.getFeedbackSummary(options?)
Aggregated feedback buckets (rating distribution).
const summary = await hub.traces.getFeedbackSummary();
Returns FeedbackSummaryResult ({ data: FeedbackBucket[] }).
traces.listFeedback(options?)
Paginated feedback list across all traces.
const feedback = await hub.traces.listFeedback({ limit: 10 });
Returns FeedbackListResult ({ data: FeedbackEntry[], total, page, limit }).
traces.getTraceFeedback(traceId)
All feedback for a specific trace.
const fb = await hub.traces.getTraceFeedback(traceId);
Returns TraceFeedbackResult ({ data: FeedbackEntry[] }).
gateway.flush() / gateway.close()
await hub.gateway.flush(); // wait for background trace writes to finish
await hub.gateway.close(); // flush, then release the exit hook
gateway.chat(), streaming, and gateway.runToolLoop() 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 flushes at process exit.
gateway.close() is idempotent and also supports await using hub = new AcruxCore(...).
Tool catalog (hub.tools)
Catalog operations live on hub.tools.
tools.sync(tools, options?) / tools.syncOne(tool, options?)
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([getWeather]);
const one = await hub.tools.syncOne(getWeather, { onConflict: 'error' });
| Parameter | Type | Notes |
|---|---|---|
tools / tool | AcruxTool | From acrux.tool. |
options.onConflict | 'warn' | 'error' | Default 'warn'. 'error' throws when a commit supersedes a dashboard-authored version. |
Returns ToolSyncResult ({ toolId, versionNumber, committed, alias, supersededSource? }).
committed is false on a cache hit.
tools.resolve(refs)
Resolve catalog refs to schemas plus executor types in one request.
const [resolved] = await hub.tools.resolve([{ name: 'get_weather', alias: 'production' }]);
// resolved.toolId, resolved.executorType ('client' | 'http'), resolved.function
Returns ResolvedTool[] ({ toolId, versionNumber, executorType, function }).
Throws API_ERROR (404) when any ref does not resolve.
tools.execute(toolId, args, options?)
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.
const { result, latencyMs, toolVersionId } = await hub.tools.execute(toolId, { city: 'Lahore' }, {
alias: 'production',
traceId,
parentSpanId: llmSpanRef,
});
| Parameter | Type | Notes |
|---|---|---|
toolId | string | From resolve(). Required. |
args | Record<string, unknown> | The model's parsed arguments. Required. |
options.alias | string | Which alias to run. |
options.versionNumber | number | Pin an exact version. |
options.traceId | string | Attach the span to this trace. |
options.parentSpanId | string | Nest under this span (normally the llm span). |
Returns ToolExecuteResult ({ result, status, latencyMs, toolVersionId }).
tools.list(options?)
List tools for the team, newest first.
const page = await hub.tools.list({ search: 'weather', limit: 10 });
| Parameter | Type | Notes |
|---|---|---|
options.search | string | Free-text search on name. |
options.page | number | 1-based page. |
options.limit | number | Page size. |
Returns { data: ToolSummary[], total, page, limit }.
tools.get(id)
Fetch one tool's shell by id.
const tool = await hub.tools.get(toolId);
Returns ToolDetail ({ id, name, description, latestVersion?, createdAt, updatedAt }).
tools.create(input)
Create a new tool shell. Commit a version with commitVersion to give it a schema/executor.
const tool = await hub.tools.create({ name: 'get_weather', description: 'Weather lookup' });
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Must match ^[a-zA-Z0-9_-]{1,64}$, unique per team. |
description | string | no | Human-readable. |
Returns ToolDetail.
tools.update(id, input)
Update a tool's name and/or description. Does not touch versions.
const updated = await hub.tools.update(toolId, { description: 'Updated description' });
Returns ToolDetail.
tools.delete(id)
Soft-delete a tool. Versions and aliases are preserved but unreachable. Returns void.
await hub.tools.delete(toolId);
tools.commitVersion(toolId, input)
Commit a new immutable version for a tool. The first commit auto-creates production and staging aliases.
const version = await hub.tools.commitVersion(toolId, {
description: 'Get weather for a city',
parametersSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
executor: { type: 'client' },
});
| Field | Type | Required | Notes |
|---|---|---|---|
description | string | no | Model-facing description. |
parametersSchema | Record<string, unknown> | yes | JSON Schema for the tool's parameters. |
executor | { type: 'client' } | { type: 'http'; ... } | yes | How the tool runs. |
changelog | string | no | Release note for humans. |
Returns ToolVersionDetail.
tools.listVersions(toolId, options?)
List a tool's versions, newest first. Items omit parametersSchema/executor.
const versions = await hub.tools.listVersions(toolId);
Returns { data: ToolVersionSummary[], total, page, limit }.
tools.getVersion(toolId, versionNumber)
Fetch one version with its full parametersSchema/executor.
const v = await hub.tools.getVersion(toolId, 1);
Returns ToolVersionDetail.
tools.promoteAlias(toolId, alias, versionNumber)
Promote an alias to point at a specific version. Creates the alias if it doesn't exist.
const alias = await hub.tools.promoteAlias(toolId, 'production', 2);
Returns ToolAliasDetail ({ alias, versionNumber, toolId }).
tools.analytics(options?)
Read aggregated call analytics (count, error rate, p50/p95 latency) per tool.
const analytics = await hub.tools.analytics({ since: '2026-01-01T00:00:00Z' });
| Parameter | Type | Notes |
|---|---|---|
options.since | string | ISO-8601 start bound. |
options.until | string | ISO-8601 end bound. |
Returns ToolAnalyticsResult ({ data: ToolAnalyticsEntry[] }).
acrux.tool
Declare a tool whose interface and implementation live in one value. The model
sees the name and parameters schema; your handler runs when it calls the
tool. Works with a zod schema (typed args) or a plain JSON
Schema object (untyped args).
import { acrux } from '@acruxcoreai/sdk';
import { z } from 'zod/v4';
const getWeather = acrux.tool(
{
name: 'get_weather',
description: 'Get the current weather for a city.',
parameters: z.object({ city: z.string() }),
alias: 'production',
},
async ({ city }) => ({ tempC: 21 }),
);
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Must match ^[a-zA-Z0-9_-]{1,64}$. |
description | string | no | What the model reads. Omit to let the dashboard own it. |
parameters | z.ZodSchema | Record<string, unknown> | yes | zod v4 schema or a JSON Schema object. |
alias | string | no | Catalog alias a sync moves. Default 'production'. |
changelog | string | no | Release note for humans; never shown to the model. |
The second argument is handler: (args) => unknown | Promise<unknown>.
acrux.tool() itself throws TOOL_SCHEMA_ERROR for an invalid name. The zod
schema is converted to JSON Schema lazily at sync time, so ZOD_NOT_AVAILABLE
(zod given but not installed) and the TOOL_SCHEMA_ERROR for a classic zod v3
schema surface from tools.sync() / the first loop call — not from acrux.tool().
Structured output
responseFormat asks the model for a typed answer. Pass an OpenAI-shaped dict,
or build one from a zod schema with the { zod, name, strict? } variant — the
SDK converts it to JSON Schema at send time.
await hub.gateway.chat({
model: 'agent-model',
messages,
responseFormat: { zod: z.object({ sentiment: z.enum(['pos', 'neg', 'neutral']) }), name: 'sentiment' },
});
| Variant | Shape |
|---|---|
| text | { type: 'text' } |
| json_object | { type: 'json_object' } |
| json_schema | { type: 'json_schema', json_schema: { name, schema?, strict? } } |
| zod | { zod: ZodSchema, name, strict? } |
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.
BYO provider
Route a call directly to your own OpenAI-compatible endpoint instead of the
gateway — the hop and its latency are skipped, and apiKey is sent only to
baseUrl, never to AcruxCore.
const hub = new AcruxCore({
apiKey: process.env.ACRUXCORE_API_KEY,
baseUrl: process.env.ACRUXCORE_BASE_URL,
provider: { baseUrl: 'https://api.groq.com/openai/v1', apiKey: process.env.GROQ_API_KEY },
});
Pass provider on the constructor (a default for every call) or per-call on
gateway.chat()/gateway.runToolLoop(). 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.costUsd and gateway.cache are always null (the gateway
never saw the call). Throws PROVIDER_ERROR for a non-2xx provider response.
Span shapes
IngestSpan (passed to traces.ingest()):
| Field | Type | Notes |
|---|---|---|
spanId | string | Required; opaque, unique within the trace. |
parentSpanId | string | Links to another span's spanId. |
name | string | Required. |
kind | SpanKind | 'llm' | 'tool' | 'retrieval' | 'embedding' | 'agent' | 'chain' | 'other'. |
status | SpanStatus | 'ok' | 'error' | 'unset'. |
startTime | string | Required; ISO-8601 with offset. |
endTime | string | ISO-8601. |
model, provider | string | Model/provider metadata. |
usage | { promptTokens?; completionTokens?; totalTokens?; cachedTokens? } | Token usage. cachedTokens is the part of promptTokens served from the provider's cache, priced at the cached rate. |
costUsd | number | Cost in USD. |
promptVersionId | string | Prompt lineage. |
variables | unknown | The prompt variables behind input. Read back when feedback becomes a dataset example. |
input / output | unknown | Stored only with payload capture on. |
attributes | Record<string, unknown> | Free-form. |
error | string | Error message for failed spans. |
Error codes
All failures throw an acruxcoreError with a code field — use it for
programmatic handling rather than matching on the message.
| Code | When it is thrown |
|---|---|
MISSING_API_KEY | No apiKey in args or env; or a BYO provider.apiKey is empty. |
MISSING_BASE_URL | No baseUrl in args or env; or a BYO provider.baseUrl is empty. |
NETWORK_ERROR | All retries exhausted at the network level. |
API_ERROR | Non-retryable HTTP error from the gateway (4xx, or 5xx after retries). Inspect statusCode/body. |
MISSING_VARIABLES | Template requires variables you did not supply. |
TOOL_SCHEMA_ERROR | acrux.tool: invalid name, an unsupported parameters shape, or a classic zod v3 schema. |
MISSING_DISPATCH | runToolLoop: a tool has no implementation (thrown before the first model call). |
ZOD_NOT_AVAILABLE | A zod schema was given but zod could not be imported. |
PROVIDER_ERROR | BYO: non-2xx response from your provider endpoint. |
Sessions (hub.sessions)
sessions.list(options?)
List sessions with pagination.
const page = await hub.sessions.list({ limit: 10 });
| Parameter | Type | Notes |
|---|---|---|
options.page | number | 1-based page. |
options.limit | number | Page size. |
Returns { data: SessionSummary[], total, page, limit }.
sessions.get(sessionId)
Get session detail with all traces.
const session = await hub.sessions.get(sessionId);
Returns { session: SessionSummary, traces: TraceSummary[] }.
Evaluations
Datasets (hub.datasets)
| Method | Description |
|---|---|
datasets.create({ name, overallFeedback? }) | Create a dataset. Returns DatasetDto. |
datasets.buildFromFeedback({ name, feedbackIds, overallFeedback? }) | Build a dataset from trace feedback. Returns BuildFromFeedbackResult. |
datasets.list() | List all datasets. Returns DatasetDto[]. |
datasets.get(id) | Get dataset with examples. Returns DatasetWithExamples. |
datasets.update(id, { name?, overallFeedback? }) | Update dataset metadata. Returns DatasetDto. |
datasets.delete(id) | Delete a dataset. Returns { success: boolean }. |
datasets.addExample(datasetId, { input, criteria?, history? }) | Add an example to a dataset. Returns DatasetExampleDto. |
datasets.removeExample(datasetId, exampleId) | Remove an example. Returns { success: boolean }. |
Experiments (hub.experiments)
| Method | Description |
|---|---|
experiments.create({ datasetId, versionIds, models, judgeModel, promptId?, name?, alias?, judgePromptId? }) | Create an experiment. judgeModel is required: the model that grades every result. judgePromptId is an optional prompt that grades instead of the built-in judge. Returns ExperimentDto. |
experiments.list() | List all experiments. Returns ExperimentDto[]. |
experiments.get(id) | Get an experiment. Returns ExperimentDto. |
experiments.startRun(experimentId) | Start a run for an experiment. Returns StartRunResult ({ runId, status }). |
Runs (hub.runs)
| Method | Description |
|---|---|
runs.list({ status?, datasetId?, promptId?, page?, limit? }) | List runs. Returns RunListResponse. |
runs.get(id) | Get run detail. Returns RunDetailDto. |
runs.getReport(id) | Get run report. Returns RunReport. |
runs.getCell(id, cellKey) | Get a specific cell. Returns RunCellDetailDto. |
runs.getCandidate(id, candidateId) | Get a candidate. Returns CandidateDetail. |
runs.promoteCandidate(id, { promptCandidateId, alias? }) | Promote a candidate. Returns PromoteResult. |
Optimize (hub.optimize)
| Method | Description |
|---|---|
optimize.start(promptId, { datasetId, models, judgeModel, draftCount?, alias? }) | Start prompt optimization. judgeModel is required. Returns StartOptimizeResult ({ runId, status, promptMismatchWarning? }). |
Where to next
- Quickstart — make your first call in ten minutes.
- Create a tool — put a tool in the catalog, from code or from the dashboard.
- Chat, stream, and collect feedback with the SDK.
- Python SDK reference — same methods, Python style.