Changelog¶
All notable changes to techrevati-runtime are documented here. The format
follows Keep a Changelog; the project
follows Semantic Versioning, with the
caveat that 0.x APIs are explicitly unstable.
[0.2.1] — 2026-05-20¶
Sharp-edges patch landed the same day as 0.2.0 to close silent footguns
identified in the 0.3.0 migration audit. No new primitives; one
intentional soft-breaking semantic change to GuardrailViolatedError
(callers reading the legacy single-violation fields still work — see
"Changed" below).
Fixed¶
RecoveryRecipe.step_retriesis now honored byattempt_recoveryandaattempt_recovery. Previously the field existed on the dataclass but the recovery executors did not consume it — a recipe that setstep_retries={RecoveryStep.RETRY_WITH_BACKOFF: 3}silently ran the step exactly once. Now the executor retries the step up to the budgeted count before moving to the next step. Missing keys default to a single attempt, preserving 0.2.0 behavior.OpenTelemetrySinkcleans up orphan parent spans on interpreter exit. If a process died betweenAGENT_STARTED/PHASE_STARTEDand the matchingAGENT_COMPLETED/AGENT_FAILED/PHASE_COMPLETED, the parent span previously stayed open in the exporter buffer and corrupted the APM trace tree. Anatexithook now marks every still-open parent witherror.type=abrupt_terminationand anERRORstatus before ending it. The hook is no-op on the clean-exit path.
Added¶
register_pricing(model, pricing, *, on_conflict="overwrite")— explicit merge semantics."overwrite"(default) preserves 0.2.0 behavior;"error"raisesPricingAlreadyRegisteredErroron re-registration;"keep"retains the existing entry and drops the new pricing silently. Useful for "register defaults if not present" startup patterns.PricingAlreadyRegisteredError— exported fromtechrevati.runtime. Subclass ofValueError, carries.model.GuardrailViolationdataclass — one entry in the newGuardrailViolatedError.violationstuple. Carriesoutcome,guardrail(name),stage("pre"/"post"). Hasto_dict()for audit-log serialization.DeprecationWarningonOrchestrator(...)instantiation — emitted once per process.AgentSessionhas been the canonical class name since 0.2.0; the alias remains for a deprecation window and will be removed in 0.3.0. Silent in import — only the first construction warns.
Changed¶
GuardrailViolatedError.violations— every guardrail that fires at the same stage is now collected and surfaced as a tuple on the raised error, instead of short-circuiting on the first violation. Required for EU AI Act Article 12 record-keeping (audit logs must reflect the full set of guardrails that fired). Legacy callers that readerror.outcome/error.guardrail/error.stagestill work — those attributes mirror the first violation. The orchestrator now runs every pre-check and post-check before raising; tests that asserted short-circuit behavior have been updated.
[0.2.0] — 2026-05-20¶
Durable execution, token-aware rate limiting, OTel agent-level span
nesting, granular usage limits, supply-chain hardening. Zero new
runtime dependencies. Two soft-breaking changes: OTel sink wire format
(one-shot → nested) and UsageLimitExceededError for non-cost
overruns (see Migrating from 0.1.x).
Added — Sprint 6 (supply chain + release polish)¶
- CycloneDX SBOM in
release.yml— generated viacyclonedx-bom(dev-only) before publish and attached to the GitHub Release as both JSON and XML. PyPI Trusted Publishing already attaches a Sigstore-backed attestation to each artifact;SECURITY.mdnow documents thegh attestation verifycommand callers should run before installing. .github/workflows/codeql.yml— Pythonsecurity-and-qualityCodeQL scan on every push, PR, and weekly cron. Findings go to the repo's Security tab.- Zero-deps smoke job in
ci.yml— installs the built wheel into a fresh venv with no[dev]or[otel]extras and imports the full public surface on Python 3.11 / 3.12 / 3.13. Guards the zero-runtime-dependency promise against accidental optional-deps leakage in__init__. examples/durable_agent.py— full SqliteSaver + thread_id + idempotency_key + ProviderRouter + RateLimiter demo. Runs twice in a row to show resume-from-checkpoint replay.examples/parallel_tools.py—arun_parallel_toolsunderasyncio.TaskGroupwith input-order results.examples/pricing.jsonpopulated — six representative 2025-Q4 entries (premium / mid / mini tiers) with_verified_ontimestamp and a demonstration of the new 5-min / 1-hour ephemeral cache-write tiers. Model identifiers are intentionally generic so callers can drop in their own provider names without diff noise.
Added — Sprint 5 (usage limits, prompt caching TTL, scheduler, async policy, persistent sinks)¶
UsageLimits— per-session token / tool-call / cost caps with Pydantic-AI-compatible field names (request_tokens_max,response_tokens_max,total_tokens_max,tool_calls_max,cost_usd_max). Wired intoAgentSession(usage_limits=...); each turn callstracker.check_limitspost-record.UsageLimitExceededError— distinct fromBudgetExceededError. Both share the newUsageBoundExceededErrorbase class so callers can choose to handle them together or separately.UsageSnapshot.cache_ttlandUsageSnapshot.tool_calls— optional ephemeral-cache TTL hint ("5m"/"1h"/None) and a per-turn tool-call counter fortool_calls_maxaccounting.ModelPricing.cache_write_5min_per_million/cache_write_1h_per_million— 2026 ephemeral prompt-caching tiers.UsageTracker.cost_for_turnpicks the rate viaModelPricing.write_rate_for_ttl; unknown TTL falls back to the legacy single-tiercache_write_per_million.scheduler.py—Clockprotocol,SystemClock(default production),ManualClock(canonical deterministic test double, promoted fromtests/conftest.pywith newtick,now_utc,sleep_asynchelpers).persistence.py—SqliteEventSinkandSqliteUsageSinksatisfy the existingEventSink/UsageSinkprotocols, persist to stdlibsqlite3in WAL mode, and survive process restart. Fills the long-running-session gap that the in-memory ring buffers can't.PolicyEngine.evaluate_async— awaits asyncmatcheswhile running sync conditions in place.AsyncOrchestrationSessioncallers can now plug coroutine-based policy rules in.
Changed — Sprint 4 (OTel nesting + AgentSession rename)¶
AgentSessionis the canonical class name (formerlyOrchestrator). The legacyOrchestratorsymbol is now a bare alias for the same class — same constructor, same identity. It will be removed in 0.3.0; new code should importAgentSessiondirectly.OpenTelemetrySinknow emits nested spans instead of one-shot events.AGENT_STARTED/PHASE_STARTEDopen a long-lived parent span keyed by(role, phase); subsequent events emit as children of that parent via OTel context propagation;AGENT_COMPLETED/AGENT_FAILED/PHASE_COMPLETEDend it, copying the terminal event's attributes (incl.error.typeand anERRORstatus on failure) onto the parent. APM dashboards now see real trace trees per session instead of unrelated event roots. See Migrating from 0.1.x.- New
docs/migrating-from-0.1.x.mdwalks the rename + the OTel wire format change.
Added — Sprint 3 (rate limiting + routing + structured concurrency)¶
TokenBucket/AsyncTokenBucket— classic token-bucket limiters with injectable clock. Sync usesthreading.Lock; async usesasyncio.Lock+asyncio.sleepso waiting yields the event loop.RateLimiter/AsyncRateLimitercompose three named buckets (rpm,input_tpm,output_tpm) so typical LLM-provider limits fit in one object.RateLimitExceededErrorraised on timeout.Orchestrator(rate_limiter=...)/Orchestrator(async_rate_limiter=...)— wired intorun_turnandarun_turn. RPM is spent before the call; input / output TPM after theUsageSnapshotis known, matching how providers enforce limits.ProviderRouterprotocol with three reference implementations:StaticProviderRouter(wraps the existingnext_provider),RoundRobinProviderRouter(strict rotation),WeightedProviderRouter(highest-weight non-excluded, ties to declaration order).Orchestrator(provider_router=...)exposes it on sessions; caller code consults it when a recovery step calls for a switch.RecoveryRecipe.step_retries— optional per-step retry budget the caller is expected to honor when executing a step. Default empty mapping preserves the 0.1.0 single-attempt semantics.AsyncOrchestrationSession.arun_parallel_tools(...)— runs a sequence of tool coroutines concurrently underasyncio.TaskGroup. Any child failure cancels its siblings and surfaces anExceptionGroup; atimeoutargument applies to the whole group._ainvokemigrated fromasyncio.wait_fortoasyncio.timeout— proper structured-concurrency cancellation semantics per PEP 789. The inner task is cancelled exactly once; no resurrection.- Docs:
docs/patterns/rate-limiting.md,docs/patterns/routing.md,docs/api/rate_limit.md,docs/api/routing.md, all in the nav.
Added — Sprint 2 (durable execution)¶
CheckpointSaverprotocol —get/put/list/deleteshape that mirrors the LangGraph contract. Two reference impls ship:InMemorySaver(process-local) andSqliteSaver(path)(durable via stdlibsqlite3, no new runtime dependency). Both are thread-safe;SqliteSaveruses WAL mode so concurrent readers don't block the writer.Orchestrator(saver=...)+session(thread_id=...)/asession(thread_id=...)— pair the two to turn a session into a restart-resumable workflow. Per-turn checkpoints are written automatically when both are configured.run_turn(..., idempotency_key=...)/arun_turn(..., idempotency_key=...)— replay-safe turns. A matching key on the samethread_idshort-circuits the call and returns the cached(result, usage)without invoking the model.docs/patterns/durability.md+docs/api/checkpoint.md— when / when-not / anti-patterns / tuning + mkdocstrings API reference.
Fixed — Sprint 0 (code-quality + bug fixes)¶
- Release pipeline gating —
.github/workflows/release.ymlnow runsruff+mypy --strict+pytest+ per-module coverage on 3.11/3.12/3.13 BEFORE the PyPI publish step. Pre-0.2.0 the publish step could ship a wheel that failed CI on the underlying commit. _resolve_pricingread race —usage_tracking._resolve_pricingnow snapshotsPRICING_TABLEunder_pricing_lockbefore the prefix-match loop, closing theRuntimeError: dictionary changed size during iterationwindow that opened whenever a thread calledregister_pricingwhile another resolved a model.- Hard turn timeout was blocking —
OrchestrationSession._invoke_fnno longer useswith ThreadPoolExecutor(...) as ex:, whose__exit__callsshutdown(wait=True)and madeTurnTimeoutErrorwait for the slow worker thread to return. We now callshutdown(wait=False, cancel_futures=True)in finally so the timeout propagates promptly. classify_exceptionwalks the exception chain — wrapped errors (raise MyAppError() from ConnectionError(...)) are now classified by the original cause's type. Cyclic chains are detected and broken. Type-based dispatch is consolidated into_EXCEPTION_TYPE_MAPPING.RecoveryRecipe.stepsis nowtuple[RecoveryStep, ...]— frozen dataclass contract is honored end-to-end. Construction from alistis still accepted (auto-converted in__post_init__) for back-compat.__version__sourced from package metadata — single source of truth inpyproject.toml;importlib.metadata.version()with a local-checkout fallback so editable installs keep working.- CI build matrix —
.github/workflows/ci.ymlbuild job now runs on Python 3.11/3.12/3.13 (was 3.11 only), matching the test matrix.
Fixed — Sprint 1 (docs trust)¶
mkdocs.ymlsite description — "(alpha)" → "(beta)" to match the pyproject.toml classifier, README, and CHANGELOG.docs/index.md— added a!!! warning "Beta"admonition pointing at the migration guide so the landing page no longer reads as "Production"..github/workflows/docs.yml—mkdocs build→mkdocs build --strictso broken refs and unresolved nav entries fail the build instead of silently degrading the published site.CONTRIBUTING.md— new "Testing" section covering when to reach for Hypothesis property tests and how to use theManualClockinjection pattern fromtests/conftest.py.CODEOWNERS— added an inline note flagging that the@Techrevati/runtime-maintainersteam must exist on GitHub for auto-review to actually trigger.
[Pre-0.2.0] — Sprint 6 (testing rigor)¶
Hardening work between 0.1.0 and 0.2.0. Public API is unchanged; this is test-suite and CI-only.
Added¶
- Hypothesis as a dev dependency. Two new property test modules:
tests/test_property_retry_policy.pyexercisesclassify_exceptionover arbitrary strings and verifiesbackoff_delayinvariants across all four jitter modes;tests/test_property_circuit_breaker.pydrives theCircuitBreakerstate machine through random op sequences with the injectableManualClockand asserts invariants the example-based tests could not enumerate. - pytest-randomly as a dev dependency. The default test runner now shuffles test order on every invocation, surfacing hidden order dependencies. Suite passes deterministically under shuffled ordering.
ManualClocktest double promoted from per-module duplication intest_circuit_breaker.py/test_async_circuit_breaker.pytotests/conftest.pyas both an importable class and amanual_clockfixture. Sprint 8 rate-limiter / scheduler primitives that accept an injectable monotonic clock will plug into it without re-inventing the type.scripts/check_module_coverage.py— per-module coverage floor checker. Wired into.github/workflows/ci.ymlto fail builds when any module insrc/techrevati/drops below 85% statement coverage. The global--cov-fail-under=90did not catchpermissions.pyslipping to 82% in 0.1.0; this closes that gap.
Changed¶
tests/test_permissions.pynow coversPermissionOutcome.to_dict()for both the minimal and fully-populated cases.permissions.pymoves from 82% → 100% statement coverage; aggregate suite coverage 93.79% → 94.91%.
0.1.0 — 2026-05-20¶
First beta release. Closes the primitive-parity gap with 2026 agent
SDKs and ships the async path the 0.0.x docstring had been falsely
advertising. APIs in this release are intended to be stable; breaking
changes between 0.1.x and 0.2.0 will be documented in
docs/migrating-from-0.0.x.md and
gated by deprecation warnings.
Consolidates the work of pre-release tags 0.1.0.dev1, 0.1.0.dev2,
0.1.0.dev3, and 0.1.0.rc1. Per-sprint detail is in the git log.
Added — Async path¶
AsyncCircuitBreakermirrorsCircuitBreakersemantics withasyncio.Lock, samehalf_open_max_probesserialization, injectable monotonicclock. State independent of the sync variant.Orchestrator.asession()returns anAsyncOrchestrationSession.arun_turnandarun_tooldrive async coro factories; sync helpers (authorize,evaluate_policy,evaluate_gate,summary, lifecycle methods) shared with the sync session via_SessionBase.aattempt_recovery(scenario, ctx, *, sleeper=asyncio.sleep)async sibling ofattempt_recoverywith injectable sleeper contract.arun_turn(timeout=...)enforces deadlines withasyncio.wait_for; syncrun_turn(timeout=...)uses a one-shotThreadPoolExecutor. Both raiseTurnTimeoutErrorfor a single catchable error class across code paths.AgentStatus.CANCELLEDterminal state.asyncio.CancelledErrorout ofasync with orch.asession()transitions the worker to CANCELLED and re-raises.AsyncOrchestrationSession.pause_for_input(prompt)async human-in-the-loop hook. Transitions worker toWAITING_FOR_INPUT; resolve from elsewhere viasession.provide_input(value).RUNNING → WAITING_FOR_INPUTis now a valid transition (was missing in 0.0.x).
Added — Industry primitive parity¶
MaxIterationsExceededError+Orchestrator(max_iterations=25)cap. Default matches OpenAI Agents SDK; counted across bothrun_turnandarun_turn. Stopping conditions are an industry production-readiness requirement.Handoffimmutable dataclass (techrevati.runtime.handoffs) +OrchestrationSession.handoff_to(target_role, reason, context). Finalizes the source worker as COMPLETED, registers a fresh worker for the target role under the same project_id, returns a Handoff describing the delegation. Enables the orchestrator-workers delegation pattern on top of our primitives.GuardrailProtocol +GuardrailOutcome+GuardrailViolatedError(techrevati.runtime.guardrails).Orchestrator(guardrails=[...])auto-runscheck_prebefore andcheck_postafter everyrun_tool/arun_toolinvocation; first violation raises with guardrail name, stage, role, tool. Mirrors the OpenAI Agents SDK guardrail model.AllowAllGuardrailreference no-op implementation.AgentSessionalias forOrchestrator. The 0.2.0 rename will promoteAgentSessionto the canonical name withOrchestratorkept as a deprecation alias; adopting the new name now is forward-compatible.
Added — Observability¶
EventSinkandUsageSinkProtocols (techrevati.runtime.sinks), plusNoopEventSink,NoopUsageSink,RingBufferEventSink,RingBufferUsageSinkdefaults.RingBufferEventSinkenforces a configurable capacity (default 1000) so long-running sessions can't balloon memory — closes the unbounded-tracker gap from 0.0.x.Orchestrator(event_sink=..., usage_sink=...)plumbs the configured sinks through every session. EveryAgentEventthe session records is forwarded to the event sink; every recorded turn is forwarded to the usage sink with its computed cost. A misbehaving sink cannot tear down the session — emit failures log and are swallowed.OpenTelemetrySinkandOpenTelemetryUsageSink(techrevati.runtime.otel, available via the new[otel]extra). Mirrors every event as a one-shot OTel span withgen_ai.operation.name,gen_ai.provider.name,gen_ai.agent.name, optionalgen_ai.agent.id, anderror.typeon failures. Recordsgen_ai.client.token.usagehistogram (withgen_ai.token.type=input|outputdiscriminator) and atechrevati.cost.usdcounter. Span names follow the GenAI agent spans convention (create_agent/invoke_agent/execute_tool/invoke_workflow).[otel]extra:opentelemetry-api>=1.27,opentelemetry-sdk>=1.27,opentelemetry-semantic-conventions>=0.48b0.- Structured
logger.infocalls at five decision points: recovery attempted, session failed, quality gate failed, handoff issued, budget exceeded. All withextra={role, phase, project_id, ...}so log shippers can pivot by role.
Added — Docs and DX¶
docs/tutorials/end-to-end.mdwalks every primitive composed together with sync, async, and OTel switchover.examples/tiny_agent.pyrunnable companion (not bundled in wheel). Smoke-tested end-to-end.examples/pricing.jsonreference template with illustrative vendor pricing (not normative).docs/api/*.mdeightmkdocstrings-backed API reference pages via the Python handler.docs/patterns/orchestrator.mdrewritten with When-to-use / Anti-patterns / Tuning template + a prominent naming-disambiguation callout separating ourOrchestratorfrom the orchestrator-workers delegation pattern.CONTRIBUTING.md,SECURITY.md,CODEOWNERS,.github/dependabot.yml,.github/ISSUE_TEMPLATE/{bug,feature}.md.
Changed¶
evaluate_policy(elapsed_seconds=...)is nowfloat | None(defaultNone). When omitted, elapsed is auto-computed from session start soTimedOutconditions finally fire. Callers passing explicit0.0previously must migrate to pass the value they actually want.AgentRegistryand_SessionBaserecord session start time on construction.- README revised end-to-end. Headline pitch matches what the
package now does (sync and async, four standard primitives,
OTel GenAI semconv). New "Why not LangGraph / OpenAI Agents
SDK?" positioning section. Classifier bumped from
3 - Alphato4 - Beta. - README tagline reflects beta status.
Notes¶
- Tool input gating is pre-call site (role + tool name) gating + post-call value gating. True input-value gating arrives when we have a typed tool input model (post-0.2.0).
- Guardrail violations are not retried automatically — they raise.
- Span nesting (parent/child relationships across agent/turn/tool)
is not yet emitted — discrete spans +
gen_ai.agent.idgive correlation. Nesting is targeted for 0.2.0. - The
[dev]extra now installs OpenTelemetry SDK packages so the optionalotelmodule type-checks and tests run undermypy --strict.
0.0.1 — 2026-05-20¶
Fixed¶
mypy --strictnow passes. Addedsrc/techrevati/__init__.pynamespace marker (PEP 420) so the wheel layout no longer double-maps modules. Removedcontinue-on-error: truefrom the CI mypy step, which had been silently swallowing failures since 0.0.0.CircuitBreakerusestime.monotonicinstead oftime.timefor duration checks. NTP/clock jumps no longer stick the breaker open or close it early.- Removed inaccurate
"Production async runtime"claim from the package docstring. Async support is targeted for 0.1.0; this version is sync only.
Added¶
BudgetExceededErrorplusOrchestrator(enforce_budget=True)flag. When enabled,run_turnraises after the cumulative cost exceedsbudget_usd. The default remains backwards-compatible (records an event, returns normally) so existing callers see no behavior change unless they opt in.has_pricing(model)helper.UsageTracker.record_turnnow emits a one-timeWARNINGper process per model when pricing has not been registered. This closes the silent-$0 footgun where unregistered models produced no cost signal.CircuitBreaker.half_open_max_probes(default1). Concurrent half-open probes are now serialized; previously the lock was released beforefn()ran, letting unbounded threads stampede a recovering service. Probe in-flight counting is tracked under the same lock as state transitions. Conforms to the Polly default; raise to N for Resilience4j-style behavior.CircuitBreaker.clockparameter accepting aCallable[[], float]. Defaults totime.monotonic. Test code injects a manual clock to make recovery-window tests deterministic;time.sleepis no longer used in the test suite.backoff_delay(jitter=...)accepts"none","full","equal", or"decorrelated"mode strings. Bool values are still accepted for backward compatibility (Truemaps to"full",Falseto"none"). Newcapandprev_delayparameters support standard AWS Architecture Blog formulas (Marc Brooker, exponential backoff & jitter).- README "Limitations" section documenting sync-only constraint, in-memory tracker growth, pricing-not-bundled default, advisory permissions, lack of durable execution, and lack of OTel integration.
Changed¶
- Default jitter algorithm is now decorrelated (was full-additive 25%
jitter). Per AWS Builders' Library, decorrelated is the fastest of the four
documented algorithms. The change affects code calling
backoff_delay()with default jitter; passjitter="equal"for behavior closest to the previous default. - README tagline reflects alpha status until 0.2.0 (was "Production runtime primitives ...").
[project.optional-dependencies] devnow pinspytest,pytest-cov,mypy, andruffto exact versions matching.pre-commit-config.yaml. Local lint and CI lint can no longer disagree.- CI now installs the package with
pip install -e ".[dev]"instead of unpinnedpip install pytest pytest-cov ruff mypy.actions/setup-pythonbumped from v4 to v5 with pip caching.codecov/codecov-actionbumped from v3 to v4. ThePYTHONPATH=srcworkaround in pytest is gone (now resolved by the namespace marker).
0.0.0 — 2026-05-20¶
Initial public release under the techrevati-runtime namespace.
Provides the foundational primitives for orchestrating multi-step LLM agent loops with reliability and cost visibility:
Orchestrator+OrchestrationSession— single-loop wiring of lifecycle, usage, retry classification, circuit breaker, permissions, and policy.CircuitBreaker— three-state fault-tolerant execution wrapper.RecoveryContext+attempt_recovery+classify_exception— failure classification and recipe lookup.UsageTracker+register_pricing+load_pricing_from_file— per-model cost tracking with caller-provided pricing (no bundled pricing data).QualityGate+QualityLevel— graduated pass/fail evaluation.AgentRegistry+AgentWorker— validated lifecycle state machine.AgentEvent— typed lifecycle events with an OpenTelemetry attribute bridge.PermissionPolicy+PermissionEnforcer— deny-first role × tool gating.PolicyEngine+ composable conditions — declarative rule evaluator.