This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.
# Releases
## llama-agents-server@0.6.4
### Patch Changes
- c42b064: Retry transient Agent Data read failures and use a longer
default timeout.
Co-authored-by: llama-org-ci-bot[bot] <231146559+llama-org-ci-bot[bot]@users.noreply.github.com>
AgentDataClient used the default httpx timeout on its shared client, so
a slow Agent Data search could fail after 5 seconds as a `ReadTimeout`
with little useful message. Cloud persistence reads through this path,
and search is the only idempotent Agent Data operation in this client.
This branch gives the shared client a 30 second read timeout with a 5
second connect timeout, then retries transient search failures with
bounded exponential backoff. The retry path covers transport failures
and 500/502/503/504 responses. Create, update, and delete paths still
make one attempt because Agent Data writes do not have idempotency keys;
replaying an ambiguous write timeout could duplicate or overwrite data.
The retry code keeps timeout and attempt config type-stable, validates
attempt counts, and has server coverage for retryable reads, final
failure, 4xx handling, and all write methods as non-retried. It also
adds a patch changeset for `llama-agents-server`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Adrian Lyjak <adrianlyjak@gmail.com>
Every `ctx.store` write copies the whole live state before the edit, so
a concurrent reader keeps seeing the committed state until the
`edit_state` block finishes. The copy is a deep copy, and that is the
problem. State often holds live objects that cannot be deep-copied. A
`Memory` wraps sqlalchemy, aiosqlite, and tiktoken modules. An LLM
client wraps a `threading.Lock`. Deep-copying either one throws:
- `wf.run(memory=Memory.from_defaults(...))` raises `TypeError: cannot
pickle 'module' object` (#709)
- `await ctx.store.set("client", some_client)` raises `TypeError: cannot
pickle '_thread.lock' object` (#710)
The deep copy only exists to isolate data a reader could otherwise catch
mid-edit. A live handle like memory or an LLM client is shared by
reference across the whole run anyway, so copying it gains no isolation.
It just crashes. So copy each entry on its own, and when one will not
deep-copy, keep the reference instead.
Plain data like dicts, lists, and scalars still deep-copies, so edit
isolation is unchanged for anything a reader could actually observe
torn. Typed state that is not a `DictState` still copies whole-model,
falling back to a shallow copy if some field refuses to deep-copy
instead of raising.
Fixes#709, fixes#710.
Rehydrating a persisted `WorkflowFailedEvent` could take down the whole
event/context reload. `_deserialize_exception` rebuilt the original
exception with `cls(message)`, but only caught `(ImportError,
AttributeError, ValueError)`. Any exception whose constructor doesn't
take a single positional message raised `TypeError`, which escaped
`model_validate` and crashed `JsonSerializer.deserialize`.
`json.JSONDecodeError(msg, doc, pos)` and `LLMError(error_type,
message)` are the common cases. One awkward exception type on a failed
run made that run unloadable.
Reconstruction is now total. It returns an `Exception` for any input and
never raises. When the real type can't be rebuilt, it degrades to a new
`UnreconstructedException` carrying the original qualified type name and
message. A type can't be rebuilt when it is unimportable, disallowed by
the allowlist, or has a constructor that rejects `cls(message)`.
```python
exc = json.JSONDecodeError("Expecting value", "doc", 0)
reloaded = roundtrip(WorkflowFailedEvent(exception=exc, ...))
# before: TypeError escapes, the whole reload dies
# after: UnreconstructedException, .original_type == "json.decoder.JSONDecodeError",
# str(reloaded.exception) == str(exc)
```
`str(UnreconstructedException(msg))` is the original message with no
type prefix, so a re-serialize cycle doesn't double-wrap it. Importable
single-argument exceptions still round-trip by type identity.
## Allowlist gating
The same code path had an asymmetry worth closing. Every other
deserialized type runs through `JsonSerializer`'s `allowed_types` gate,
but the exception path called `import_module_from_qualified_name`
directly. A checkpoint blob could name any importable `module.Class` and
the loader would import and call it. Exception types now honor the same
allowlist. `builtins.*` is exempt, an unset allowlist stays permissive
to match how `allowed_types` already works, and anything else has to be
a member or it degrades without importing.
Reconstruction also only constructs genuine `Exception` subclasses. The
qualified name comes from the blob and could resolve to any callable.
Without that check the `builtins` exemption would run
`builtins.eval(message)` on an attacker-controlled message even under a
strict allowlist. A blob naming a non-exception builtin now degrades
instead of being called.
The allowlist reaches the validator through a `ContextVar` set around
`model_validate`, not pydantic's `model_validate(context=...)`. The
event models (`DictLikeModel`) define a custom `__init__`, and pydantic
drops the validation context before the nested field validators run, so
the context never arrives at the exception field.
Gating is best-effort by design. It covers the top-level failure
carriers (`WorkflowFailedEvent`, `StepFailedEvent`, `StepWorkerFailed`).
Serialization still stores only type and message, so `original_type` is
a breadcrumb that is dropped on re-serialization.
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.
# Releases
## llama-index-utils-workflow@0.11.0
### Minor Changes
- aee5fda: Add typed runtime step identities.
### Patch Changes
- 44887eb: Sanitize slashes in Mermaid execution diagram step IDs.
## llama-index-workflows@2.22.0
### Minor Changes
- cb89120: Add `list[E]` fan-out returns and `list[E]` fan-in joins.
- fd223e8: Steps can declare multiple single-event parameters to fire
once when one of each has arrived.
- aee5fda: Add typed runtime step identities.
### Patch Changes
- 34e166c: Fix idle checks racing buffered events and stale
`ctx.collect_events()` firings.
- 5724404: Preserve retry state for in-progress workflow steps across
serialized context resume.
- 58e0174: Fix ctx.store.get() returning bound dict methods instead of
stored values for state keys named items, keys, values, or get
## llama-agents-client@0.3.9
### Patch Changes
- Updated dependencies [34e166c]
- Updated dependencies [cb89120]
- Updated dependencies [fd223e8]
- Updated dependencies [5724404]
- Updated dependencies [58e0174]
- Updated dependencies [aee5fda]
- llama-index-workflows@2.22.0
## llama-agents-server@0.6.1
### Patch Changes
- 71a1aac: Fix startup resume incorrectly failing newly-created handlers
before their first persisted tick
- Updated dependencies [34e166c]
- Updated dependencies [cb89120]
- Updated dependencies [fd223e8]
- Updated dependencies [5724404]
- Updated dependencies [58e0174]
- Updated dependencies [aee5fda]
- llama-index-workflows@2.22.0
- llama-agents-client@0.3.9
Co-authored-by: llama-org-ci-bot[bot] <231146559+llama-org-ci-bot[bot]@users.noreply.github.com>
`control_loop.py` was a single 2128-line module fusing two unrelated
concerns — the stateful async runtime and the pure reducer — with the
reducer dominated by two functions that read as walls:
`_process_step_result_tick` (~400 lines) and `_process_add_event_tick`
(~230 lines). You couldn't follow either in one pass.
This splits the module into a package along the reducer pattern's
natural seam and breaks the two god-functions into named phases. No
behavior change: the reducer is pure, so the existing transformation and
collection-stream suites pin command ordering and stream accounting.
## Package split
`control_loop.py` becomes `control_loop/`:
- `runner.py` — the async runtime: worker tasks, the scheduled-wakeup
heap, the adapter, turning reducer commands into side effects.
- `reduce.py` — the pure reducer: `_reduce_tick` and every per-tick
processor, plus retry/collect/replay helpers. `State + Tick -> (State,
Commands)`.
- `streams.py` — collection-stream accounting: fan-out stream lifecycle,
open-work-item counting, collect-batch release.
The dependency graph is a DAG: `runner -> reduce -> streams`. The one
back-edge (`streams`' release path needs `reduce`'s
`_add_or_enqueue_event`) is a single inline import at that chokepoint,
commented.
The package facade exports only the cross-package API — `control_loop`,
`rebuild_state_from_ticks`, `rebuild_state_from_ticks_stream`,
`replay_ticks_stream` — the symbols sibling packages (server, dbos,
`external_context`, `step_function`) actually import. White-box tests
import the internals they exercise straight from the owning submodule
(`.reduce` / `.runner` / `.streams`). `import time` stays in the facade
because tests patch `control_loop.time.time`, and the submodule loggers
keep the `workflows.runtime.control_loop` name so caplog filters still
match.
## Decomposing the reducer functions
Both now read top-to-bottom as a sequence of named steps.
`_process_step_result_tick`:
```python
this_execution = _find_in_progress(worker_state, tick.worker_id)
rerun = _rerun_for_stale_collect_buffer(tick, worker_state, this_execution)
if rerun is not None:
return state, rerun
scope = _fan_out_scope(state, step_name, this_execution, fanned_out=fanned_out)
for result in tick.result:
_apply_step_result(result, ..., acc=acc)
acc.commands.extend(_resolve_work_item_in_stream(tick, state, scope, acc, now_seconds))
# finalize: NOT_RUNNING transition, drop from in_progress, drain queue
```
The per-result flags that were locals threaded through 400 lines now
ride on a small `_StepResultAcc` carrier, and the ~120-line
retry/`catch_error` branch is extracted to
`_schedule_retry_or_route_failure`.
`_process_add_event_tick` splits into its three real phases:
`_redeliver_collection_payload` (payload-carrying ticks route straight
to their collect target and stop), `_resolve_waiters`, then
`_route_to_accepting_steps` — with the gnarly collect-member routing
pulled into `_route_member_to_collect_step`.
One rename: `_apply_stream_work_delta` -> `_adjust_open_work_items`,
matching the `open_work_items` field it mutates.
Mermaid execution diagrams only normalized `:` and `#` in tick-derived
node IDs, so a slash in a step name was emitted into the Mermaid ID even
though the visible label rendered fine.
The renderer now runs execution IDs through the shared Mermaid ID
sanitizer, and that sanitizer also normalizes slashes for graph
rendering.
basedpyright 1.31.1 (the current pre-commit rev) can't parse Python
3.14's stdlib `string/templatelib.py` (t-strings). With a 3.14 project
venv, every file-scoped run — i.e. what pre-commit does on `git commit`
— reports `Cannot access attribute "interpolations" for class "str"`
against templatelib for *any* changed file, blocking commits unless you
`--no-verify`. Full `pre-commit run -a` and CI don't hit it (CI resolves
ubuntu's system 3.12), so it only bites local dev on newer interpreters.
This bumps the mirror rev to 1.39.7 and fixes the 12 errors the newer
version legitimately flags, all `reportPrivateImportUsage` — symbols
imported through modules that only re-export them transitively:
- `WorkflowTick` now imported from `workflows.runtime.types.ticks`
instead of `.plugin`
- `Auth`/`Environment` from `llama_agents.cli.config.schema` instead of
`._config`
- `httpx` imported directly instead of through `manage_client`
- `DeploymentNotFoundError` from `llama_agents.core.server.manage_api`
(its public export) instead of through the control-plane service module
- tests patch `dulwich.repo.Repo` and stdlib `webbrowser` directly
instead of reaching through the module under test (same objects, so
identical behavior)
1.39.7 also warns on the wildcard imports in the `llama_deploy`
back-compat shims; those are intentional re-exports and warnings don't
fail the hook, so they're left alone.
Verified the hook passes both file-scoped and full runs under 3.12,
3.13, and 3.14 venvs.
Open question for reviewers: `lint.yml` doesn't pin a Python for
`setup-uv`, so CI lint type-checks against whatever ubuntu ships
(currently 3.12). Worth pinning for determinism, but not done here.
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.
# Releases
## llamactl@0.10.2
### Patch Changes
- 06ff626: Fix `projects use <id>` and `projects get <id>` failing with
"not found" for projects outside the default org
- abc176e: Only auto-push push-mode updates from repos with the
deployment remote configured.
Co-authored-by: llama-org-ci-bot[bot] <231146559+llama-org-ci-bot[bot]@users.noreply.github.com>
Push-mode updates were too eager: `edit`, `apply`, and `update`
configured the deployment remote before pushing, so any current git repo
could become the source repo by accident.
This keeps create smooth, but makes updates require an existing
`llamaagents-<deployment>` remote before auto-pushing. `--push` is the
explicit escape hatch for linking and pushing the current repo, and
`--no-push` still skips the push entirely.
Docs now describe the safer default for push-mode deployments.
Tab completion lists projects from all orgs the user can access, but
`projects use <id>` validated against only the default org. A project
from a non-default org would tab-complete fine then fail with "Project
not found".
The issue is `_discover_organization` running before the direct-ID path,
scoping the validation query to whichever org has `is_default=True`.
Moved org discovery to the interactive listing branch only. When the
user passes a project ID directly, validation now queries all accessible
orgs (unless they explicitly passed `--org`). Same fix applied to
`projects get <id>`.
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.
# Releases
## llamactl@0.10.1
### Patch Changes
- 532a6fa: Fix inline auth recovery while saving deployments.
Co-authored-by: llama-org-ci-bot[bot] <231146559+llama-org-ci-bot[bot]@users.noreply.github.com>