Skip to content

v0.19.0

Choose a tag to compare

@basepointer basepointer released this 20 Oct 03:20

Changed (breaking): Pydantic AI agents get their prompt and skills from load_agent()

  • New: decimalai.pydantic_ai.load_agent(name, fallback=..., on_unavailable="continue")
    returns a Pydantic AI capability for Agent(capabilities=[...]). Its instructions are the
    agent's saved prompt ({{placeholders}} filled from the run's deps), then the skill list
    routed on the run's prompt; its toolset is load_skill, limited to the agent's skills; and its
    wrap_run opens one DecimalAI run span per run, so a run's model calls are one trace filed under
    the DecimalAI agent, whatever the Agent or init(agent_name=...) is called: under
    Agent.instrument_all(), Pydantic AI's own run span is the trace's root, and it is named for the
    DecimalAI agent too. Every run is routed on its own prompt, the way OpenAI Agents runs are.
    Built only on Pydantic AI's capability API.
  • Removed: decimalai.pydantic_ai.instrument() and the three patches behind it
    (Agent.__init__ twice, Agent.iter): the skills instructions function and load_skill it
    added to every Agent in the process, the decimalai_load_skill rename, the run span it opened
    around every run, and get_current_routing_id(). Tracing is DecimalAI's OpenTelemetry exporter:
    decimalai.init(otel=True) with Agent.instrument_all(), or a provider instrumentor
    (decimalai.init(openai=True)).
  • An agent that also has the Pydantic AI harness's Skills gets no DecimalAI skills from the
    capability, with one warning. An agent with another tool named load_skill fails its run with
    Pydantic AI's own error.
  • The harness's observed Skills (decimalai.pydantic_ai_harness.skills()) needs no
    instrument() either: with init(otel=True) and Agent.instrument_all(), each run's skill
    catalog and every load go on that run's trace.
  • decimalai init --framework pydantic-ai writes the new shape.

Migration:

# before
from decimalai.pydantic_ai import instrument
instrument(enable_skill_loader=True)
agent = Agent(model, name="support", system_prompt=BASE, tools=[lookup_order])

# after
from decimalai.pydantic_ai import load_agent
support = load_agent("support", fallback=BASE)
agent = Agent(model, name=support.name, tools=[lookup_order], capabilities=[support])

Changed (breaking): decimalai.load_agent() returns the agent's prompt and skills

  • decimalai.load_agent(name, fallback=..., on_unavailable="continue") returns an AgentConfig
    that works like decimalai.openai_agents.load_agent(), for code that calls a model itself:
    system(message, context=...) is this turn's system text (the saved prompt with its
    {{placeholders}} filled from context, then the skill list routed on message), tool_spec
    is the load_skill tool as {name, description, parameters}, and load_skill(name) returns a
    skill's text. prompt, prompt_version, prompt_hash, prompt_source, variables and
    skill_names describe what it holds. Inside an OpenTelemetry run, each turn's record (prompt
    version, placeholder names, skills offered and loaded) goes on that run's trace.
  • Removed: the prompt-only AgentConfig it returned before (system_prompt,
    version_number, content_hash, is_fallback, …) and load_agent()'s version= and
    cache_ttl_seconds=. support.prompt is what config.system_prompt was, except that it is
    the fallback= text when no prompt is saved.
  • decimalai init files and the Support project read support.prompt.
  • decimalai.MissingPromptVariable is importable from the top level.
  • The OpenTelemetry exporter keeps DecimalAI's own record attributes on the exported span:
    decimal.agent and decimal.prompt.* (version, hash, placeholder names, never their values).

Migration:

# before
config = decimalai.load_agent("support")
agent = create_agent(model, tools=tools, system_prompt=config.system_prompt)

# after
support = decimalai.load_agent("support")
agent = create_agent(model, tools=tools, system_prompt=support.prompt)

Fixed: a refusing platform gets fewer retries, spread out

  • A retry after a 429 or a 502/503/504 now waits a random time under a ceiling that doubles
    (1 s, 2 s, 4 s, capped at 8 s), on top of any Retry-After, instead of exactly 1, 2 and 4 s.
    Processes refused at the same moment no longer come back at the same moment.
  • When a manifest registration is refused, the background sender's re-registration makes one
    request per step instead of running the client's own retries inside each step, and waits
    before its first step. One trace whose registration stays refused now sends at most 9
    registrations, down from 24 on most adapters, 32 on the generic tracer and 72 on LangChain.
    The steps still span 31-62 s, long enough to bridge a backend restart.
  • The generic tracer and the LangChain adapter no longer repeat a registration that the client
    has already retried (429, 502-504) or that a retry cannot fix (other 4xx, a plan quota).

Added: assign a skill to an agent, and create it restricted, from SkillRouter

  • SkillRouter.assign_skill(agent_name, skill_id, pin_version_id=None) assigns one of your
    skills, or a public registry skill, to one agent (POST /api/v1/agents/{name}/skills/{id}).
    Assigning again updates the pin. unassign_skill(agent_name, skill_id) removes it (with a key
    that has admin permission). Before, this took a raw REST call.
  • create_skill(..., offer_scope="restricted") creates a skill offered only to the agents it
    is assigned to, in one call, so it is never offered org-wide in between. Against an older
    backend, which ignores the field, it follows up with update_skill(offer_scope=...), the
    call you used to make yourself.
  • get_trace()'s type lists the skill fields the API returns. skills_delivered holds names
    only; the delivered versions and where each body sits in the prompts are
    skills_delivered_versions and skills_delivered_sites (older backends mixed them into
    skills_delivered).