Skip to Content
MigrationsLoom 0.16 → 0.17

Migrating from 0.16 → 0.17

Read this if you are upgrading an app from Loomflow 0.16. In 0.17 every subsystem was checked against its reference (provider SDKs and docs, the MCP / A2A / OpenTelemetry specs, the papers behind the architectures, the Agent Skills spec) and each defect found was fixed. Most of that needs nothing from you, but some defaults changed. This page lists the changes that can make existing code behave differently, and what to do about each. The changelog  has everything else.

Models

  • effort= reaches every current Claude model. Claude 5.x, Fable, Opus 4 / 4.1 and Haiku 4.5 used to drop it with a warning; they now think at the level you set, and you pay for the thinking tokens. Each model gets the levels it accepts (minimal becomes low on Claude), and an effort string that isn’t one of the six levels raises ValueError. See Reasoning effort.
  • Retries. RetryPolicy uses full jitter by default: each delay is drawn from 0 up to the backoff. Pass RetryPolicy(jitter=0.1) for the old ±10%. The SDK clients loomflow builds no longer retry on their own, an OpenAI insufficient_quota 429 is no longer retried, and a Retry-After longer than max_delay_s ends the retries instead of sleeping. See RetryPolicy.
  • Claude structured output uses native JSON outputs on Claude 4.5 and later, so a run with output_schema= can call its tools, where it used to be forced straight to the answer. See Providers.
  • Gateway ids are priced (0.17.1). anthropic/claude-haiku-5.5, openai/gpt-5-mini and the like used to cost $0, so max_cost_usd budgets never tripped. They are now priced as the vendor’s model, and from 0.17.2 get its capabilities too. See Gateways.

Permissions and hooks

  • Mode.ACCEPT_EDITS auto-approves only the built-in write and edit tools. Every other destructive tool, bash included, now asks. Name your own editing tools with StandardPermissions(edit_tools=[...]). See Permissions.
  • An approval handler approves only with True, an action name ("allow", …) or an ApprovalDecision. Anything else, such as "deny" or {"approved": False}, used to be read with bool() and approved; it now denies.
  • A pre-tool hook that times out or raises denies the call. A timed-out hook used to count as allow, and a raising one crashed the run. HookRegistry(timeout_denies=False) restores the old timeout behaviour. See Hooks.

MCP

  • A tool name that two servers share is exposed as server__tool (was server.tool), because providers reject . in tool names. The dotted name still resolves. A name only one server has stays bare. See MCP.

Durable runs

  • A finished run’s steps never replay into a later run on the same session. A run that raised still replays when you run it again, and Agent.resume still replays the last run. See Runs and epochs.
  • Journals are JSON. SqliteRuntime and PostgresRuntime skip rows pickled by earlier releases, so those steps run again, unless you pass serializer="pickle". Only do that for a store no untrusted party can write.
  • Journals and checkpoints are kept per user. Runs without a user_id still read what earlier versions wrote. A run with a user_id won’t replay journal entries or resume checkpoints that earlier versions wrote for its session. Agent.list_checkpoints takes user_id=.
  • Signals. wait_for_signal(..., timeout_s=) raises TimeoutError. Undelivered signals are kept for at most 10,000 sessions and an hour (max_pending_signal_sessions, pending_signal_ttl_s on InProcRuntime and JournaledRuntime).

Sandboxes

  • FilesystemSandbox resolves relative paths against its first root (base=), not the process’s working directory.
  • OSSandbox gives the tool a minimal environment, not the parent’s, where your API keys live. Pass OSSandbox(env={...}) for the variables a tool needs. On macOS, Seatbelt now confines reads too: the tool sees only the system libraries, the Python installation, its own package, the roots and a private temp dir. See Sandboxes.

Telemetry and serving

  • OpenTelemetry spans follow the GenAI conventions: chat {model}, execute_tool {tool} and invoke_agent, with the old loom.* name in the loom.span.name attribute. Dashboards keyed on OTel span names need updating; the in-memory, console and JSONL sinks keep the loom.* names. Metrics are gen_ai.client.* and no longer carry session_id, turns or user_id. See Semantic conventions.
  • The serve /stream endpoint sends a failure after the stream started as event: stream_error (it reused error). See Serving.

Budgets

  • max_wall_clock and per_user_max_wall_clock count time spent running, not time since the budget was created. Code that calls allows_step / consume itself wraps its work in budget.running(). See Budget caps.

Architectures and workflows

  • Debates vote on each debater’s Final answer: line, and parse_score reads “7/10” and “score: 7” as 0.7. The Supervisor’s forward_message ends the turn.
  • Reflexion lessons stay with their user. A run recalls its own lessons plus any tagged user_id="*". Untagged lessons written by earlier versions are no longer recalled. See Reflexion.
  • Workflows check their edges when the run starts: an edge, route or default naming a node that was never added raises ValueError. A workflow stops at an Agent node that comes back interrupted, with WorkflowResult.interrupted set. See Workflow.
  • A2AClient speaks A2A v1.0 and falls back to v0.3. Pass protocol_version="0.3" to pin the old protocol. See A2A.

Config, skills and evals

  • Agent.from_config / from_dict reject unknown keys with ConfigError, naming the closest known key.
  • Skill names and descriptions are validated against the Agent Skills spec: no leading or trailing hyphen or -- in a name, no XML tags in a description. A bad SKILL.md raises SkillError.
  • An eval metric that raises fails the gate: the case records it in CaseResult.metric_errors, and passed / assert_thresholds fail.
  • The unused dbos and temporal extras are gone. Drop them from your install line.

Errors

  • Errors from agent.stream() and Workflow.parallel are raised as-is, not wrapped in an ExceptionGroup. See Errors.

0.17.2: waiting for background work

Agent.aclose() waits for the agent’s background work (the auto_extract fact extraction that runs after run() returns) and returns how many tasks were still running. Call it before reading the facts a run just produced, or at shutdown. See aclose.

0.17.3: regression fixes

Upgrade if you hit any of these on 0.17.0 to 0.17.2; no code changes are needed.

  • agent.memory.forget() didn’t stop a running background extraction, which could write the erased facts back. It now does.
  • Under SubprocessSandbox or OSSandbox, the workspace and living-plan tools failed on every call, and so did a skill’s tools.py tools. Both work now; see Sandboxes.
  • A Seatbelt child started in the parent’s working directory and failed to start when the profile couldn’t read it. It now starts in its first root, so relative paths in a tool resolve there.
  • An interrupted Agent branch of Workflow.parallel reached merge as an internal object. It now stops the run with WorkflowResult.interrupted set; see Parallelism.
  • A role model given as an OpenAI or DeepSeek id (Reflexion(evaluator_model="gpt-4.1-mini")) failed when its key was only in the agent’s Tuning(secrets=...). It now takes the key from there; see Secrets.
  • ReWOO’s default planner prompt told the model to reference steps as {En}, which the executor didn’t read, so the step got that literal text. The prompt now says {{En}}, and {En} is read too.
Last updated on