Skip to main content
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), run.message (the user turn) and run.tools (a load_skill tool), which you pass to your own Chat Completions or Responses API loop. Each run is one trace, filed under the DecimalAI agent. Nothing in the OpenAI SDK is patched, so only the requests you give these pieces to get DecimalAI’s prompt and skills. decimalai.openai needs SDK 0.25 or later; through 0.24, code that called the OpenAI SDK itself used decimalai.agent_config() (see Moving from decimalai.agent_config()). Building on the OpenAI Agents SDK (from agents import Agent) instead? See OpenAI Agents.

Install

The extra installs openai and openinference-instrumentation-openai, 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 from DECIMAL_API_KEY when decimalai.init() has not been called, and a name your workspace has no agent by is created, 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 no fallback=, 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 skills your project lists in .decimal/skills.toml, with nothing sent.
  • support.run(question) routes the agent’s skill list on question and opens the run’s trace. It calls no model: make the model calls inside the with block.
  • run.system is the system message’s text: the saved prompt, then the agent’s skill list with one sentence telling the model to load a skill before using it.
  • run.message is the user turn, {"role": "user", "content": question}. Send it instead of your own user dict: in a chat it can carry more.
  • run.tools holds the load_skill tool in Chat Completions’ shape, 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. The list is empty when the run has no skills to offer.
  • run.tool_result(call) answers a load_skill call with its tool message, and returns None for any other tool, so or hands the call to yours. 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 a sentence the model can read. run.load_skill(name) returns the same text alone, for a dispatcher of your own.
  • No decimalai.init(). With a key, agent_config() initializes the SDK from DECIMAL_API_KEY and turns its tracing on itself. init() refuses to start without a key, so a file that calls it cannot run keyless.
decimalai init <agent> --framework openai writes this loop as a runnable file, with an example tool.

The Responses API

The same run, on client.responses.create(): the system text goes in instructions, the user turn in input, and the tool from run.response_tools (the same load_skill, in the Responses API’s flat shape, strict). run.tool_result(call) answers a function_call item with its function_call_output:
A request with previous_response_id does not carry the earlier request’s instructions over, so send run.system with every request.

Async, and other clients

  • AsyncOpenAI: open the run with async with support.run(question) as run:, and answer each call with await run.atool_result(call) (await run.aload_skill(name) for the text alone), which keep the load off the event loop.
  • OpenAI-compatible endpoints: the tools are plain dicts and run.tool_result() reads a call as the SDK returns it or as a dict, so an OpenAI(base_url=...) client for Azure OpenAI, a local server or another provider’s OpenAI-compatible endpoint takes the same run, and its calls are traced.
  • LiteLLM: litellm.completion() takes run.system, run.message and run.tools the same way, and run.tool_result() answers its tool calls. Calls made through a library other than openai are not traced; the run’s record is.

Chats

A chat keeps one conversation, and a conversation’s system text and tool stay what its first message got, so every request in the chat starts with the same prefix (which OpenAI’s prompt cache reuses). A newly saved prompt or a newly assigned skill reaches the next conversation, not an open one.
  • run.message is 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, after the question. Send it instead of your own user dict, or the model is not shown what this message’s routing picked.
  • chat.state is the conversation as JSON-ready data. A server that keeps no chat in memory stores it beside the chat’s history, and support.conversation(state=saved) resumes it in another process, with the same system text and tool. A state for another agent, or state= together with variables=, raises ValueError.
  • 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"]):
A variable with no value stops the run before any model call, with a 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.

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.
A run makes one routing decision however many tool calls it takes, and every request in it sees the same list. When the agent’s whole list fits, load_skill takes only the names on it, and another name is answered with the list and loads nothing; when the list was cut to fit, it takes any name, and the platform serves a skill of the agent’s that the cap left off.

When an agent’s skills are switched off

  • The tool’s kill switch. With DECIMALAI_LOAD_SKILL_TOOL=0 or decimalai.init(load_skill_tool=False), every run is routed and the text of the skills picked for it goes into run.system instead (inject_skill_body, which defaults to on once the tool is off), and run.tools is empty. With inject_skill_body=False as well, the agent gets no skills, and one warning says so.
  • decimalai.init(enabled=False) after agent_config() stops routing and skills for that config; the prompt it read keeps running, and an open conversation keeps its system text. An agent_config() called while it is off asks DecimalAI nothing: the agent runs on its fallback= text, and without one agent_config() raises RuntimeError.
  • on_unavailable="fail". By default ("continue") a run whose skill list never came back answers without skills, and its trace says so. With agent_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: load_skill answers with a sentence the model can read.

Tracing

agent_config() turns on OpenAI tracing (OpenInference’s instrumentor, which wraps the SDK’s classes for the process) on the same pipeline as decimalai.init(openai=True), and sends only its runs:
  • Each run is one trace, filed under support.name, with its model calls (Chat Completions and Responses API alike), 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’s session_id. 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. An OpenAI call made outside one, in any client of the process, is held back, and so are the app’s own spans. decimalai.init(openai=True) sends every OpenAI call; decimalai.init(otel=True), the app’s spans.
  • Set up tracing first. Your own OpenTelemetry tracer provider, and decimalai.init(otel=True), go before the first agent_config(): 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.
  • Reading each trace. agent_config(..., on_trace=fn) calls fn once per run with a copy of the finished trace before it is sent. A run made inside decimalai.start_trace() is linked to it.
A run whose model calls were made after its with block logs one warning that no OpenAI request was traced inside it.

What gets captured

  • One trace per run: each model request with its input and output, and each load_skill result 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.
  • Token counts, 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 OpenAISDKAgentConfig(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 decimalai.agent_config()

Through 0.24, OpenAI code used the top-level decimalai.agent_config(): it wrapped run.tool_spec in OpenAI’s tool shape itself, answered load_skill with run.load_skill(name), and needed decimalai.init(openai=True), agent_run() and the instrumentor package for a run to reach a trace. That still works. decimalai.openai does the same in OpenAI’s shapes, and traces each run with no other line:

What’s next

Skills

Skill list vs body delivery, the load_skill tool, and the usage ladder.

OpenAI Agents

The same prompt and skills for an agent built on the OpenAI Agents SDK.