Skip to content

v0.22.0

Choose a tag to compare

@basepointer basepointer released this 20 Oct 18:05

Fixed

  • Claude Agent SDK: a skill whose body Claude Code rewrote is still its exact version.
    Claude Code rewrites a skill's body before the model reads it. It fills $ARGUMENTS, $0,
    $1, ... with the words of the Skill call's args (0-based, and with the empty string when
    the call has none), drops the backslash from \$1, and fills ${CLAUDE_SKILL_DIR},
    ${CLAUDE_SESSION_ID}, ${CLAUDE_PLUGIN_ROOT} and ${CLAUDE_PROJECT_DIR}. The hook named a
    version by hashing the delivered body against the lock, so any body that changed matched
    nothing, and its activation went out with the skill but no version_id. The prod prober found
    this on 2026-10-20: support-reply-format quotes a $12 fee, and a 13-word args replaced it
    with the 13th word. Now, when the delivered body is no version's, the hook redoes Claude Code's
    rewrite on the locked body and names that version only if the result is the delivered body byte
    for byte. The installed SKILL.md is read for this, and counts only while its body is still the
    locked one. A body that does not match still names no version; it is never guessed.

    • The rules were read from the bundled CLI (claude-agent-sdk 0.2.166, Claude Code 2.1.299) and
      checked against it: 226 args strings and 13 bodies recorded with the local stub, and one
      claude-haiku-4-5 run. Claude Code splits args into words with its own bash parser (quotes
      group words, # starts a comment, ; and | end the list). DecimalAI follows that split; it
      matched all 60 realistic args held out from writing it. A split it gets wrong costs the
      activation its version, never gives it a wrong one.
    • Not reproduced, so such a body still names no version: ${CLAUDE_EFFORT},
      ${CLAUDE_PLUGIN_DATA} and inline shell (!`cmd`), whose values only the CLI saw; and a
      cached older version loaded with arguments after decimalai skills update (the file on disk
      is then the newer version).
    • Args appended to a body with no placeholder are taken off when they start with ! too
      (Claude Code escapes it as \!), and a call without args no longer strips a body's own last
      line when that line starts with ARGUMENTS:.
  • Plain Anthropic tracing keeps the prompt and the completion on anthropic 1.15 and later.
    anthropic 1.15 traces its own API calls, and by default its span leaves the messages out
    (content_mode is metadata_only). Under decimalai.init(anthropic=True) each call then
    arrives as two spans, the SDK's inside OpenInference's, and DecimalAI kept the inner one as the
    LLM call so that a stacked pair counts once. The trace then had no user_input_preview or
    final_output_preview, its call had no rendered_input or output, and the agent's manifest
    got no system prompt. With anthropic 1.13 the same run was complete. The Claude prod prober,
    which pins anthropic 1.15.0, found it on 2026-10-20. This was not a privacy default:
    OpenInference records the messages, and DecimalAI dropped them. Now a call span with no
    content takes it from the span around it, when that span wraps this one call only.

    • The conformance anthropic driver now gives the client its own TracerProvider, so the SDK's
      spans reach the driver's exporter as they reach the global provider in production. Before,
      they went elsewhere and the driver stayed green on anthropic 1.15.0. With them, it fails C4,
      C8, C9, C14, C19 and D1 without the fix, and passes with it.
  • One lost packet no longer costs an agent its skill. SkillRouter gave every request one
    attempt, with 1 s to open the connection. The operating system re-sends a lost first packet
    after about 1 s, so a single dropped packet failed the call. Inside a turn that is silent:
    load_skill tells the model the skill is unavailable, and the agent answers without it. On
    2026-10-20 the OpenAI tutorial's skill load failed this way in 1.2 s while the platform was
    serving other requests normally. Now:

    • A connection that could not be opened is retried once, for any request (nothing reached the
      platform). The connect budget for in-turn calls (/skills/route, /skills/menu, skill
      bodies) is 2 s instead of 1 s. An unreachable platform now costs about 4 s per call instead
      of 1 s; the circuit breaker still opens after three failed calls.
    • A dropped connection or a read timeout is retried once for requests that are safe to
      repeat: GETs, assign_skill and unassign_skill. An in-turn read gets a 2 s budget for
      the retry. Routing requests are not retried after a read timeout (a slow route falls back
      to the agent's menu), and the menu fallback reads once.
    • Calls share one connection pool per process instead of opening a new connection each time.
      A run's later calls skip the TCP and TLS handshake (0.59 s → 0.32 s per call from Indonesia
      in the live check).
      A call that fails both attempts still counts once in routing_status().

Fixed: a saved prompt broke open Claude chats

Claude rejects a conversation whose system prompt or tool definitions changed once a thinking
block is in its history (HTTP 400, "The block is bound to a different conversation"), and
Claude 5.x models think by default. Every agent_config() rebuilt the prompt and skill list per
run, so a version saved in the dashboard, a skill assigned or removed, or a cut list picked
again broke the next message of every open Claude chat. Reproduced live on claude-haiku-5-5
for the four below.

  • decimalai.agent_config(): new support.conversation(variables=, state=). Each
    chat.run(message) returns a Run whose system and tool_spec are the first run's for the
    whole chat; chat.state is JSON that continues it in another process. support.run() is
    unchanged, for one-message runs and providers without the rule.
  • Pydantic AI: on a Claude model, a run given message_history= shows the instructions and
    load_skill of that conversation's first run. The copy travels in the first request's
    metadata inside result.all_messages(). No change to your code. (Pydantic AI 2.56+ retried
    the 400 by dropping the reasoning, with an AnthropicStaleThinkingBlockWarning.)
  • Google ADK: on a Claude model, a session keeps its first invocation's instruction and
    load_skill. DecimalAIPlugin keeps the copy in the session's state
    (_decimalai_conversation), so any process serving the session uses it; without the plugin
    the copy is the process's own.
  • OpenAI Agents SDK (Claude through LiteLLM): not fixed; the instructions are given no
    conversation to keep. A warning says so once when a Claude chat sends thinking back, and the
    docs give two settings that avoid the 400.
  • Later runs of a kept conversation are still routed on their own message for the record; with
    a list cut to fit, they and their loads name the decision the list came from. A newer prompt
    or list reaches the next conversation. Other models behave as before.
  • Each run's load_skill names are now fixed for the whole run, so a routing decision landing
    mid-run no longer changes the tool between one run's model calls. Run.tool_spec names the
    skills on that run's list; when the list is cut to fit it still takes any name, so a skill
    the cap left off can be loaded.