Major Changes, Major Refactor, Ready for V1
·
14 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
The public API is now covered by the deprecation policy above,
and the package is classified Development Status :: 5 - Production/Stable. This
release is the production-hardening round on top of 0.8.0: durable state gets
optimistic concurrency and an idempotent tool ledger, execution is bounded by real
timeouts and cancellation, and per-user isolation is enforced across the storage
layer.
Breaking
injectqis pinned to>=0.4.0,<0.5. It is pre-1.0, so a0.5release may
break the API; without an upper bound it would have been picked up automatically
and broken fresh installs.- Default
user_idis now"anonymous"(was"test-user-id"). A run with no
user_idpreviously filed itself under a placeholder that looks like a real
account, which, with per-user isolation enabled, silently pooled every
unauthenticated run into one identity. - A conditional edge whose condition raises now fails the run (
GraphError,
GRAPH_ROUTING_001). Previously the exception was swallowed and the graph fell
through to the first static edge orEND— silently taking a path nobody chose. - Production refuses to start with wildcard CORS and credentials enabled
(API package). Set explicitORIGINS, orCORS_ALLOW_CREDENTIALS=false.
Added
- Optimistic concurrency control on durable state.
statesnow carries a
versioncolumn withUNIQUE (thread_id, version); writes take a per-thread row
lock and compare-and-swap. A write based on a stale version raises the new
StaleStateError(HTTP 409 at the API) instead of silently discarding another
run's update. - Durable tool-execution ledger (
tool_executions, schema v3). A node replayed
after a crash no longer re-fires tool calls that already completed — the
double-charge scenario. Keyed by
(thread_id, origin_message_id:tool_call_id), because atool_call_idalone is
not unique across turns. - Per-step durable checkpointing (
durable_checkpoint_every_step, default on),
so a crash replays one node rather than the whole run. - Node and tool timeouts (
node_timeout,tool_timeout) that actually cancel
the work, plus stop-cancels-a-running-node — previously stop was only polled
between nodes, so a hang inside one was unreachable. - Real schema migrations with a stepwise, idempotent runner and a
pg_advisory_xact_lock, so concurrent workers cannot race the DDL. - Per-user isolation in the checkpointer (
enforce_user_isolation, default on)
across state, messages, and threads. - File ownership. Uploads record an owner; reads by another user 404.
- Backpressure on background tasks (
max_pending_tasks, default 1000). A slow
or dead publisher sink previously grew an unbounded task set until OOM. - OpenTelemetry metrics via
metrics.setup_otel_metrics(); counters and
histograms on node/tool execution, with outcome dimensions. - Structured, correlated logging via
logging.setup_structured_logging(); every
record carriesrun_id/thread_id/node, so one run can be grepped out of a
busy server. agentflow build --k8sgenerates a Kubernetes manifest whose termination grace
period is long enough that a rolling deploy does not kill in-flight runs.
Fixed
- Lost updates on concurrent writes to one thread (see CAS above). Reads were
also non-deterministic:ORDER BY created_at DESCwith no tiebreak. - The realtime cache could be moved backwards, wedging a thread until its TTL
expired. Cache writes are now an atomic version-guarded compare-and-set, and a
lost version check invalidates the cache so the thread self-heals. - Parallel tools clobbering each other's state. Each tool now runs on its own
branch copy, merged back field-by-field against a baseline, using a field's
reducer when it has one. - One failing tool orphaned its siblings (
gatherwithoutreturn_exceptions),
and malformed tool arguments raisedJSONDecodeErrorthrough the whole node. - Retries on non-retryable errors. Status classification matched
"500"as a
substring, somax_tokens must be <= 500was treated as a server error. - Cross-tenant reads/deletes of state, messages, threads, and files.
- Rate limit bypass. The bucket key came from the leftmost
X-Forwarded-For
entry, which the caller controls — a new value per request meant a new bucket and
no limit at all. Proxy hops are now counted from the right. - Blocking
urllib.urlopeninsideasync def(cloud media store) stalled the event
loop for every concurrent run in the process. - Connection-pool and Qdrant-collection cold-start races (double creation).
- Schema-version failures were swallowed instead of raised.
What's Changed
- feat: add Xquik MCP example and repair MCP agents by @kriptoburak in #144
- chore(deps): bump actions/cache from 4 to 5 by @dependabot[bot] in #136
- Ready for V1 by @Iamsdt in #145
New Contributors
- @kriptoburak made their first contribution in #144