Skip to main content

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.

OptionTypeDefaultNotes
apiKeystringprocess.env.ACRUXCORE_API_KEYThrows MISSING_API_KEY if neither is set.
baseUrlstringprocess.env.ACRUXCORE_BASE_URLThrows MISSING_BASE_URL if neither is set.
cacheTtlnumber (ms)60000Render cache freshness window. 0 disables caching (and serve-stale).
maxCacheSizenumber500Max LRU entries. Set by the first constructor.
maxRetriesnumber1Retries 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.
retryIntervalnumber (ms)500Delay between retries.
providerProviderConfig—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' },
);
ParameterTypeRequiredNotes
namestringyesPrompt slug (not id).
aliasstringyese.g. 'production', 'staging'.
variablesRecord<string, unknown>noTemplate 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 });
ParameterTypeNotes
options.searchstringFree-text search on name.
options.pagenumber1-based page.
options.limitnumberPage 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' });
FieldTypeRequiredNotes
namestringyesUnique per team.
descriptionstringnoHuman-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',
});
FieldTypeRequiredNotes
messagesMessage[]yesChat messages (may contain {{variables}}).
modelstringnoDefault model for this version.
tools{ toolId: string }[]noAttach 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 haveCall
Messages you wrote, no toolsgateway.chat
Messages you wrote, and the tools named on the callgateway.runToolLoop
A prompt from the catalogprompts.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 inRuns inPass
Your code, declared with acrux.toolYour processtools: [getWeather]
The catalog, http executorAcruxCoreNothing — the prompt's binding is enough, or toolRefs: [{ name }]
The catalog, client executorYour processclientTools: { get_weather: getWeather }
Nowhere — inline for this one callYour processtoolDefs: [...] 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.

ParameterTypeRequiredNotes
modelstringyesModel public name.
messagesMessage[]yesChat messages.
toolsToolDefinition[]noInline OpenAI-shaped tool definitions.
toolRefs{ name; alias? }[]noCatalog tool references (resolved server-side).
toolChoice'auto' | 'none' | 'required' | { type:'function'; function:{name} }noHow the model uses tools.
responseFormatResponseFormatnoStructured output. Mutually exclusive with tools/toolChoice.
temperaturenumbernoSampling temperature.
maxTokensnumbernoMax completion tokens.
reasoningEffortstringnoHow hard a reasoning model thinks, e.g. 'low' or 'high'. Sent as reasoning_effort. Not in 0.17.0; comes in the next release.
streambooleannoReturn an async iterable of ChatChunk instead of ChatResult.
extraBodyRecord<string, unknown>noMore 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.
providerProviderConfignoPer-call BYO override.
gateway{ maxRetries?: number; fallback?: boolean }noPer-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.
promptVersionIdstringnoFrom prompts.render().versionId; stamped on the trace span.
variablesRecord<string, unknown>noFrom 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.
traceboolean | { traceId?; parentSpanId?; name?; sessionId?; tags?; metadata? }noDefault 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.

note

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],
});
ParameterTypeRequiredNotes
modelstringyesModel public name.
messagesMessage[]yesSeed messages.
toolsAcruxTool[]noTools from acrux.tool. Run locally; reconciled with the catalog.
toolDefsToolDefinition[]noRaw OpenAI definitions, sent inline; route to dispatch.
toolRefs{ name; alias? }[]noCatalog refs. http executor runs on the platform.
clientToolsRecord<string, (args) => unknown>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 one arguments object.
dispatch(name, args) => unknown | Promise<unknown>noFallback runner. Required for toolDefs, and the fallback for a client ref with no matching tools or clientTools entry.
syncbooleannoReconcile tools with the catalog first. Default true.
maxIterationsnumbernoMax round-trips. Default 10.
temperaturenumbernoSampling temperature.
maxTokensnumbernoMax completion tokens.
reasoningEffortstringnoHow hard a reasoning model thinks, e.g. 'low' or 'high'. Sent as reasoning_effort. Not in 0.17.0; comes in the next release.
responseFormatResponseFormatnoShapes the final answer; may be combined with tools (gather + shape).
traceboolean | { traceId?; name?; sessionId? }noDefault true.
providerProviderConfignoPer-call BYO override.
gateway{ maxRetries?: number; fallback?: boolean }noPer-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.
promptVersionIdstringnoStamped on every llm span this loop records.
variablesRecord<string, unknown>noStamped 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 },
});
ParameterTypeRequiredNotes
renderedRenderResultyesFrom prompts.render. Positional.
modelstringnoOverrides the version's bound model.
messagesMessage[]noOverrides the rendered messages.
toolRefs{ name; alias? }[]noOverrides the prompt's bindings entirely. [] runs the prompt with no tools.
clientToolsRecord<string, (args) => unknown>noThe prompt's client-executor tools, keyed by name. Its http tools need no entry — the platform runs them.
streambooleannotrue 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 } },
],
});
ParameterTypeRequiredNotes
traceIdstringnoOmit to mint a new trace; pass one to append.
sessionIdstringnoGroups traces into a session.
namestringnoTrace name.
capturePayloadsbooleannoForce payload capture on for this trace.
tagsstring[]noSet on creation; merged (union) on append.
metadataRecord<string, unknown>noSet on creation; shallow-merged on append.
spansIngestSpan[]yesThe 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.

OptionTypeDefaultNotes
kindSpanKind'chain'llm, tool, retrieval, embedding, agent, chain or other.
inputunknownnone (wrap: the arguments)wrap() records the call's arguments as an array.
captureInput / captureOutputbooleantruefalse records no input / output.
sessionIdstring—Session for the trace.
tags / metadatastring[] / 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:

FieldTypeNotes
traceIdstringRequired.
spanIdstringScope to one span.
rating1 | -11 (thumbs up) or -1 (thumbs down).
labelstringShort label.
commentstringFree 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:

FieldTypeNotes
from / tostringISO date range.
statusSpanStatus'ok' | 'error' | 'unset'.
modelstringFilter by model.
sessionIdstringFilter by session.
promptVersionIdstringFilter by prompt version.
minLatencyMsnumberMinimum latency.
minCostUsdnumberMinimum cost.
minTokensnumberMinimum total tokens.
qstringFree-text search.
errorTypeSpanErrorTypeTraces with a span that failed this way, e.g. 'tool_declared'. Not in 0.19.0; comes in the next release.
errorCodestringTraces with a span carrying this declared error code. Not in 0.19.0; comes in the next release.
hasWarningbooleantrue for traces with a warning, false for none. Not in 0.19.0; comes in the next release.
tagsstring[]Traces carrying all of these tags. Not in 0.19.0; comes in the next release.
metadataRecord<string, string>Traces whose metadata has every one of these pairs. Not in 0.19.0; comes in the next release.
page / limitnumberPagination (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' });
ParameterTypeNotes
options.group_bystringFacet key to group by (e.g. 'model', 'status').
options.since / options.untilstringISO-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' });
ParameterTypeNotes
tools / toolAcruxToolFrom 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,
});
ParameterTypeNotes
toolIdstringFrom resolve(). Required.
argsRecord<string, unknown>The model's parsed arguments. Required.
options.aliasstringWhich alias to run.
options.versionNumbernumberPin an exact version.
options.traceIdstringAttach the span to this trace.
options.parentSpanIdstringNest 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 });
ParameterTypeNotes
options.searchstringFree-text search on name.
options.pagenumber1-based page.
options.limitnumberPage 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' });
FieldTypeRequiredNotes
namestringyesMust match ^[a-zA-Z0-9_-]{1,64}$, unique per team.
descriptionstringnoHuman-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' },
});
FieldTypeRequiredNotes
descriptionstringnoModel-facing description.
parametersSchemaRecord<string, unknown>yesJSON Schema for the tool's parameters.
executor{ type: 'client' } | { type: 'http'; ... }yesHow the tool runs.
changelogstringnoRelease 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' });
ParameterTypeNotes
options.sincestringISO-8601 start bound.
options.untilstringISO-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 }),
);
FieldTypeRequiredNotes
namestringyesMust match ^[a-zA-Z0-9_-]{1,64}$.
descriptionstringnoWhat the model reads. Omit to let the dashboard own it.
parametersz.ZodSchema | Record<string, unknown>yeszod v4 schema or a JSON Schema object.
aliasstringnoCatalog alias a sync moves. Default 'production'.
changelogstringnoRelease 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' },
});
VariantShape
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()):

FieldTypeNotes
spanIdstringRequired; opaque, unique within the trace.
parentSpanIdstringLinks to another span's spanId.
namestringRequired.
kindSpanKind'llm' | 'tool' | 'retrieval' | 'embedding' | 'agent' | 'chain' | 'other'.
statusSpanStatus'ok' | 'error' | 'unset'.
startTimestringRequired; ISO-8601 with offset.
endTimestringISO-8601.
model, providerstringModel/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.
costUsdnumberCost in USD.
promptVersionIdstringPrompt lineage.
variablesunknownThe prompt variables behind input. Read back when feedback becomes a dataset example.
input / outputunknownStored only with payload capture on.
attributesRecord<string, unknown>Free-form.
errorstringError 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.

CodeWhen it is thrown
MISSING_API_KEYNo apiKey in args or env; or a BYO provider.apiKey is empty.
MISSING_BASE_URLNo baseUrl in args or env; or a BYO provider.baseUrl is empty.
NETWORK_ERRORAll retries exhausted at the network level.
API_ERRORNon-retryable HTTP error from the gateway (4xx, or 5xx after retries). Inspect statusCode/body.
MISSING_VARIABLESTemplate requires variables you did not supply.
TOOL_SCHEMA_ERRORacrux.tool: invalid name, an unsupported parameters shape, or a classic zod v3 schema.
MISSING_DISPATCHrunToolLoop: a tool has no implementation (thrown before the first model call).
ZOD_NOT_AVAILABLEA zod schema was given but zod could not be imported.
PROVIDER_ERRORBYO: 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 });
ParameterTypeNotes
options.pagenumber1-based page.
options.limitnumberPage 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)​

MethodDescription
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)​

MethodDescription
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)​

MethodDescription
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)​

MethodDescription
optimize.start(promptId, { datasetId, models, judgeModel, draftCount?, alias? })Start prompt optimization. judgeModel is required. Returns StartOptimizeResult ({ runId, status, promptMismatchWarning? }).

Where to next​