agent_config() reads an agent’s prompt and skills from DecimalAI and hands them over per user message, as a run: run.system (the saved prompt, then the agent’s skill list) and run.tools (a load_skill tool), which you pass to the SDK’s Tool Runner or your own messages.create() loop. Each run is one trace, filed under the DecimalAI agent. Nothing in the Anthropic SDK is patched, so only the requests you give these pieces to get DecimalAI’s prompt and skills. agent_config() needs SDK 0.23 or later; through 0.22 the module patched messages.create() instead (see Moving from SDK 0.22).
Install
anthropic>=1.13.0 and openinference-instrumentation-anthropic, which agent_config() uses to trace its runs.
Use
agent_config("support-agent")reads the agent’s saved prompt when it is called. It initializes the SDK fromDECIMAL_API_KEYwhendecimalai.init()has not been called, and a name your workspace has no agent by is created (SDK 0.24+), so a misspelled name makes a new agent rather than an error.fallback=is what runs when the agent has no saved prompt yet, when no DecimalAI key is set, or when DecimalAI cannot be reached at startup; with a key and nofallback=, an unreachable DecimalAI raises instead of starting an agent with no prompt. With no key at all, the agent runs without a DecimalAI key: the project’s skills and public registry matches, no traces.support.run(question)routes the agent’s skill list onquestionand opens the run’s trace. It calls no model: make the model calls inside thewithblock. A Tool Runner sends nothing until it is consumed, so consume it there too (until_done(), or aforloop over it).run.systemis a list of text blocks: the saved prompt, then the agent’s skill list with one sentence telling the model to load a skill before using it. The last block carries a prompt-caching breakpoint, so a run’s later requests read the first one’s cache. See Prompt caching.run.toolsholds theload_skilltool for the beta Tool Runner, limited to the agent’s skills. The model calls it with a skill’s name and gets the skill’s full text back, which counts as delivered and activated on the usage ladder. Each load names the run’s routing decision, and the platform decides what it serves: a skill your org owns, has installed or has assigned to the agent loads whether or not this run’s list showed it, while one reachable only through the public registry loads only when this run was offered it. A refusal (an unknown name, an outage) comes back as text the model can read, markedis_error.support.nameis the agent’s name in DecimalAI, and the name every run is routed and traced for.- No
decimalai.init(). With a key,agent_config()initializes the SDK fromDECIMAL_API_KEYand turns its tracing on itself.init()refuses to start without a key, so a file that calls it cannot run keyless.
Your own messages.create() loop
client.messages.create() takes plain tool definitions: add *run.tool_params (the same load_skill as a plain definition, in a list that is empty when the tool is switched off) and answer each tool_use block with run.tool_result(block), which returns None for a block that is not load_skill:
client.messages.stream() works the same way. On AsyncAnthropic, open the run with async with support.run(question) as run:: run.tools are then async tools for the async Tool Runner, and await run.atool_result(block) answers a block. A load from a worker thread (a thread pool running your tools) is recorded on its run.
Chats
A chat keeps one conversation, and a conversation’ssystem and tools stay byte for byte what its first message got. Claude rejects a request whose system prompt or tool definitions changed once a thinking block is in the history (HTTP 400, “The block is bound to a different conversation”), and Claude 5.x models think by default. So a newly saved prompt or a newly assigned skill reaches the next conversation, not an open one.
run.messageis the user turn to send: the question, plus, for an agent whose skill list is cut to fit, the skills routed for this message that the chat has not shown yet, as a second text block. Send it instead of your own user dict, or the model is not shown what this message’s routing picked.chat.stateis the conversation as JSON-ready data. A server that keeps no chat in memory stores it beside the chat’s history, andsupport.conversation(state=saved)resumes it in another process, with the same system blocks and tool. A state for another agent, orstate=together withvariables=, raisesValueError.- Variables are the conversation’s: they are filled once, at its first message. Put facts that change per message in the user message.
Change the prompt without redeploying
agent_config() keeps a local copy of the prompt and re-reads it in the background at most once a minute, so a version saved in the dashboard’s Prompt tab reaches the next run within a minute, with no redeploy (an open conversation keeps its first message’s). Pinning an older version there, or serving the latest again, works the same way.
A saved prompt can hold variables, written {{name}}, filled from variables= as attributes or dict keys ({{customer.name}} reads variables["customer"]["name"]):
MissingPromptVariable error (decimalai.MissingPromptVariable) that names it. DecimalAI fills only the prompt, never a skill’s text, and the trace keeps the placeholder names, not their values.
Prompt caching
run.system carries a prompt-caching breakpoint at its end, so a run’s later requests (each tool round trip) read the first request’s cache. When the agent’s skill list is cut to fit the message, runs share the prompt but not the list, so a second breakpoint goes after the prompt block too, unless the prompt has variables (a filled prompt differs per customer). That is two of a request’s four breakpoints. agent_config(..., cache_control={"type": "ephemeral", "ttl": "1h"}) sets the breakpoint’s value, and cache_control=None sends none. A cache needs a prefix of at least 512 tokens on Claude Haiku, Sonnet and Opus 5.5 (4,096 on Haiku 4.5).
How each run’s skills are picked
Each run’s skill list is routed on that run’s own message.- The first run is routed before it opens.
- Later runs show the local copy of the list, and their own routing decision is recorded in the background, so no run waits on it. Closing the run does not wait either: the trace is sent once the decision lands.
- An agent with more skills than the list holds is routed before every run, so the list is picked for the message.
- A later message of a conversation shows the conversation’s list. When the list was cut to fit, the message is still routed on its own text, and the skills it picks that the chat has not shown go into
run.message. Each load names the decision that showed its skill.
When an agent’s skills are switched off
- The tool’s kill switch. With
DECIMALAI_LOAD_SKILL_TOOL=0ordecimalai.init(load_skill_tool=False), every run is routed and the text of the skills picked for it goes intorun.systeminstead (inject_skill_body, which defaults to on once the tool is off), andrun.toolsis empty. Withinject_skill_body=Falseas well, the agent gets no skills, and one warning says so. decimalai.init(enabled=False)afteragent_config()stops routing and skills for that config; the prompt it read keeps running, and an open conversation keeps its system blocks. Anagent_config()called while it is off asks DecimalAI nothing: the agent runs on itsfallback=text, and without oneagent_config()raisesRuntimeError.on_unavailable="fail". By default ("continue") a run whose skill list never came back answers without skills, and its trace says so. Withagent_config(..., on_unavailable="fail")opening the run raises instead.
- Loading a skill costs a model request. The model asks for the skill in one request and uses it in the next.
- An outage during a run is not an error. If DecimalAI cannot be reached mid-run,
load_skillanswers with a sentence the model can read. - Skills come from DecimalAI, not from disk. Anthropic’s own Agent Skills (
container={"skills": [...]}) are another way to give an agent skills; an agent given both shows the model two lists.
Tracing
agent_config() turns on Anthropic tracing (OpenInference’s instrumentor, which wraps the SDK’s classes for the process) on the same pipeline as decimalai.init(anthropic=True), and sends only its runs:
- Each run is one trace, filed under
support.name, with its model calls, the prompt version, the variable names it filled, and the skills offered, loaded and delivered. A conversation’s runs share its id as the trace’ssession_id, so a chat’s messages group together. A run opened inside your own span (a web server’s request span) is a trace of its own, linked to that span. - Nothing outside a run is sent. A Claude call made outside one, in any client of the process, is held back, even under the app’s own request span, and so are the app’s own spans.
decimalai.init(anthropic=True)sends every Claude call;decimalai.init(otel=True), the app’s spans. - Set up tracing first. Your own OpenTelemetry tracer provider, and
decimalai.init(otel=True)orinit(crewai=True), go before the firstagent_config(), as they do beforeinit(anthropic=True): OpenTelemetry sets the global provider once. - Another tool’s tracing. When the instrumentor was turned on outside DecimalAI (Phoenix, Langfuse, your own setup), DecimalAI does not turn it on again; its runs are traced as long as that tracing sends its spans to the global tracer provider, and a run that got none of its calls says so in a warning.
- Reading each trace.
agent_config(..., on_trace=fn)callsfnonce per run with a copy of the finished trace before it is sent. A run made insidedecimalai.start_trace()is linked to it.
with block (a Tool Runner consumed later) logs one warning that no Claude request was traced inside it. On AnthropicVertex and the legacy AnthropicBedrock client, the Tool Runner’s requests are not traced; a hand-written messages.create() loop is.
What gets captured
- One trace per run: each model request with its input and output, and each
load_skillresult as a tool message in the next request’s input - The prompt version, the variable names it filled, any skill outage, and the skills offered, loaded and delivered, each delivery with the exact version the model received
- An auto-detected manifest: the model and each tool, by name. Not the system text: a run’s prompt is the saved one, whose version each trace names.
- Cache reads and writes, from each response’s usage
Checkpoint: run the agent from Use once with a question that matches one of your skills, then
print(support). It names the prompt version the agent ran and the skills on its list, for example AnthropicAgentConfig(name='support-agent', prompt_version=3, skills=['refund-policy']), and the run appears on the Traces page under that name. No trace? Check that the model calls were made inside the with block. An empty skill list means the router had none to offer: check that your workspace has the skill installed or forked, and that agent_config() names the agent it is assigned to (see the silent no-ops list in the Quickstart).Moving from SDK 0.22
Through 0.22,decimalai.anthropic.instrument(enable_skill_loader=True) patched Messages.create(), .stream() and .parse() (sync and async) for every Anthropic client in the process, putting a skill menu, and sometimes one skill’s text, into each request’s system. 0.23 removed instrument() (now a stub that raises TypeError naming agent_config()), skill_system() and get_current_routing_id():
system= text becomes the fallback=: the prompt saved in the dashboard takes its place, and the fallback runs while none is saved. A chat that sent its history with every request keeps one support.conversation() beside that history and opens chat.run(question) for each message. decimalai init --framework anthropic writes a project in this shape.
What’s next
Skills
Skill list vs body delivery, the load_skill tool, and the usage ladder.
Claude Agent SDK
Tracing agents built on Claude Code as a library.