-
Notifications
You must be signed in to change notification settings - Fork 0
Releases: runcycles/langchain-runcycles
v0.3.0
f493cd8 Tool-side actual-cost extraction for CyclesToolGate — closes the final "commits at estimate" gap documented after the v0.2.0 model-gate cost_fn work.
Added
cost_fnparameter onCyclesToolGate. OptionalCallable[[ToolCallRequest, Any], Amount]. When supplied, the middleware callscost_fn(request, result)after the wrapped tool handler returns and uses the returnedAmountforcommit_reservationinstead of the configuredestimate. The request argument exposestool_call.name, args, id, and state, so one router-style extractor can price multiple tools without one gate instance per tool. When unset, behavior remains commit-at-estimate.ToolCostFntype alias exported from the package root for type-annotating user-supplied tool extractors. The existingCostFnalias remains the unary model-gate extractor shape.examples/tool_cost_fn.pydemonstrates router-style tool pricing withCyclesToolGate.cost_fn, including request-argument pricing and provider-returned metadata fallback.- Python 3.13 and 3.14 are now declared supported via PyPI classifiers. The reusable CI matrix was broadened from
["3.10", "3.12"]to["3.10", "3.11", "3.12", "3.13", "3.14"]so lint + mypy + the full test suite run against every declared version on every PR.AUDIT.md's "tested against" line was updated to match.
Resilience
CyclesToolGate.cost_fnerrors never erase the tool result. Ifcost_fn(request, result)raises or returns a non-Amount, the gate logs a warning and falls back to the configuredestimatefor the commit. The tool result is still returned to the agent. This mirrorsCyclesModelGate.cost_fnand prevents stale tool-pricing code from breaking the agent loop.
Fixed
- Source distributions now exclude local virtualenv/cache/build artifacts.
python -m buildfrom a developer checkout previously allowed local.venv*/directories into the sdist when present, producing oversized manual artifacts. Hatch now uses an explicit sdist include/exclude policy, and.gitignoreignores.venv*/.
Docs
- README,
docs/runcycles.mdx, andAUDIT.mdnow document tool-side actual-cost extraction and remove the oldCyclesToolGate"commits at estimate" limitation. The docs explicitly do not promise built-in tool extractors because tool result shapes and provider pricing are not normalized. - README "Settlement (commit) failures" and
docs/runcycles.mdx"Settlement-failure policy" now mentionCyclesModelGateparity and the non-success-CyclesResponsefailure path. The prose describedsettlement_error_policyas tool-only and exception-only; v0.2.3 already applies it to both gates and both failure modes (raised exceptions and HTTP-failure responses).AUDIT.mdandCHANGELOG.mdwere already accurate; this is a prose-drift correction with no behavior change. - README setup and error-handling guidance now matches the current examples and implementation: the Claude quick start installs
langchain-anthropicand calls outANTHROPIC_API_KEY, and the error-handling bullets cover both tool-gate and model-gate denial/reservation/handler-exception shapes.
Coverage
174 tests, 99.63% coverage (gate fail_under = 95). New tests cover sync + async CyclesToolGate.cost_fn applied / None-fallback / exception-fallback / invalid-return fallback / decide+reserve parity / decide-mode-skip, plus the new example import smoke and JSON-serialized ToolMessage.content cost extraction.
Behavior change
None for existing callers. cost_fn is additive; callers who do not pass it keep the v0.2.x commit-at-estimate behavior. Callers who opt in get actual-cost commits for tool calls, with estimate fallback on extractor failure.
Closes
Assets 2
v0.2.3
5a08a62 Correctness patch driven by external review. One real governance bug, two doc fixes, one minor lockdown. No public API change.
Fixed
-
commit_reservationandrelease_reservationHTTP-failure responses are now honored (both gates, sync + async). The runcycles SDK returnsCyclesResponse.http_error(...)on HTTP failures without raising; v0.2.0–v0.2.2 only caught raised exceptions, so a failed commit silently looked like a successful commit and bypassed the documentedsettlement_error_policycontract. The release-path was similarly silent on HTTP-failure responses. Both paths now checkresponse.is_successand apply the same policy as the exception path:- Commit HTTP failure +
settlement_error_policy="raise"(default) →RuntimeErrorcarryingdenial_reason(response). - Commit HTTP failure +
settlement_error_policy="log"→ warning logged, handler result preserved. - Release HTTP failure (best-effort) → warning logged, never raised (regardless of policy).
Log message wording changed from "commit failed" to "commit raised" / "commit returned HTTP failure" so operators can distinguish the two failure modes in audit logs. Locked down by
tests/test_tool_gate.py::test_commit_http_failure_raise_default_propagates,::test_commit_http_failure_log_swallows,::test_release_http_failure_logged, sync/async equivalents intest_model_gate.py/test_model_gate_async.py/test_tool_gate_async.py. This is the user-visible behavior change in this release — code that relied on the silent-success path will now see governance-policy responses (which is what was always documented). - Commit HTTP failure +
-
CyclesToolGatewithtool_call={"id": None}now flows throughcoerce_tool_call_id's synthesis path and producesmissing-<12-hex>. Previously thestr(tool_call.get("id", "")) or Nonepattern produced the literal string"None", creating deterministicdecide-Noneidempotency-key collisions across all malformed calls. Locked down bytests/test_tool_gate.py::test_explicit_none_tool_call_id_uses_synthetic_pathand the async sibling.
Docs
docs/runcycles.mdxCyclesToolGatesection — clarified thatactionis a single-argument callable(request) -> Actionwhilesubjectis a two-argument callable(request, state) -> Subject. The prior wording said both "resolve from request and state," which would TypeError if users followed it literally foraction.examples/tenant_budget_agent.pyinstall instructions — correctedlangchain-openai/OPENAI_API_KEYtolangchain-anthropic/ANTHROPIC_API_KEYto match theclaude-sonnet-4-6model the example actually uses.
Coverage
159 tests (was 145), 99.62% coverage (gate fail_under = 95); model_gate.py and tool_gate.py both at 100%. 14 new tests cover the four corrected paths (3 HTTP-failure scenarios ×ばつ 4 gate variants + 2 None-id regression checks).
Behavior change
Existing code that relied on silent-success commit on HTTP-failure responses will now invoke settlement_error_policy. The default policy is "raise", so callers that never set the policy explicitly will start seeing RuntimeError("Cycles commit_reservation returned HTTP failure...") propagate when the Cycles server returns a non-2xx commit response. This is the documented contract; the prior silent behavior was a bug. Set settlement_error_policy="log" to opt into the swallow-and-preserve-result behavior if that matches your UX needs.
Assets 2
v0.2.2
b8565ad Final piece of issue #13: the multi-agent / HITL / multi-tenant demo that meets LangChain's co-marketing bar. No library code change; example + write-up only.
Added
examples/multi_agent_fanout.pyrewritten to compose the full v0.2.x governance triad:CyclesFanOutGate+CyclesModelGate(withanthropic_costextractor for actual-cost commits) + LangChain'sHumanInTheLoopMiddlewareonsend_email+CyclesToolGate(indecide+reservemode). Composition order is documented inline to match LangChain's hook timing (fan-out → model → HITL review → final tool authorization). A custom state schema preserves per-tenant config so the same agent process serves many tenants with independent budgets.examples/multi_agent_fanout_writeup.md— a problem-framed pattern walkthrough leading with "pre-execution budget authority for multi-tenant agents," with the demo as a concrete proof point. Three failure-mode scenarios (full-budget tenant, fully-denied tenant, partially-allowed tenant) demonstrate how each gate independently saves the cost of everything downstream. Companion artifact for LangChain co-marketing per their stated "educational, problem-framed, multi-agent / HITL" preference.
Changed
- README "Examples" section now describes the demo with all three gates named, links the write-up.
Coverage
145 tests, 99.59% coverage (gate fail_under = 95). tests/test_examples.py import-smoke continues to gate both bundled examples on every CI run and now verifies the demo's tenant state schema, middleware order, and decide+reserve model gate mode.
Closes
Issue #13 in full: parts 1 (cost_fn + extractors), 2 (streaming verification), and 3 (demo agent + write-up) all landed across v0.2.0, v0.2.1, and v0.2.2. v0.2.x line is feature-complete for the original v0.2.0 scope.
Assets 2
v0.2.1
d3b9d35 Streaming-path verification for CyclesModelGate — closes part 2 of issue #13. No code changes; tests + docs only.
Verified
CyclesModelGate is streaming-compatible without code changes because LangChain's streaming aggregation happens inside BaseChatModel.ainvoke, which is called by _execute_model_async (langchain/agents/factory.py:1323) — the handler our awrap_model_call receives. The handler aggregates all streamed chunks into one final AIMessage with summed usage_metadata before returning the ModelResponse. Middleware never sees per-chunk callbacks.
Added
tests/test_model_gate_streaming.py(3 tests) — regression checks locking down the streaming contract on our side:test_cost_fn_called_once_when_handler_aggregates_streamed_chunks— handler internally aggregates 4 chunks into one final message;cost_fnfires exactly once. A regression that triggered per-chunk commits would fail this test.test_cost_fn_sees_aggregated_usage_metadata_not_first_chunk— handler sums chunkusage_metadatainto the final message; theopenai_costextractor computes the commitactualfrom the summed totals (250_000 microcents), not the first chunk's partial counts (which would be 50_000). Explicit!=assertion so a "first-chunk-only" regression fails loudly.test_cancellation_during_handler_releases_reservation—asyncio.CancelledErrorraised inside the handler (consumer disconnects mid-stream) triggersrelease_reservation. CancelledError is aBaseException, notException— this test guards against a future refactor that narrows theexceptclause and silently leaks reservations.
Coverage
144 tests, 99.59% coverage (gate fail_under = 95); model_gate.py, extractors.py both at 100%.
Behavior change
None. Pure verification + audit. Existing v0.2.0 callers see no change.
AUDIT.md
New "Streaming contract (v0.2.1+)" section documents the layered call path from agent.astream(...) down to our awrap_model_call, with the load-bearing fact: aggregation lives below the middleware layer, so we only ever see finalized responses.
Deferred to a later v0.2.x
examples/multi_agent_fanout.py— multi-tenant fan-out + HITL demo agent meeting LangChain's stated co-marketing bar. Tracked separately on #13.
Assets 2
v0.2.0
6bc44e4 Per-call actual-cost extraction for CyclesModelGate — closes the v0.1.x "commits at estimate" gap that prompted the v0.2.0 issue. Reserve-mode and decide+reserve-mode model calls can now debit Cycles budgets at the model's actual reported token usage, not the worst-case estimate.
Added
cost_fnparameter onCyclesModelGate. OptionalCallable[[ModelResponse], Amount]. When supplied, the middleware callscost_fn(result)after the wrapped model handler returns and uses the returnedAmountforcommit_reservationinstead of the configuredestimate. When unset, behavior is identical to v0.1.x (commit-at-estimate). The callable receives the LangChainModelResponsereturned by the handler so extractors can pullusage_metadataoff the containedAIMessage.langchain_runcycles.extractorsmodule with two factory functions:openai_cost(prompt_per_million_usd=..., completion_per_million_usd=...)— returns acost_fnthat readsAIMessage.usage_metadataand converts toUSD_MICROCENTSusing OpenAI-labeled pricing (prompt/completion).anthropic_cost(input_per_million_usd=..., output_per_million_usd=...)— same shape, Anthropic-labeled pricing (input/output). Both factories take keyword-only pricing args so callers can't accidentally swap input and output rates.
CostFntype alias exported from the package root for type-annotating user-supplied extractors.
Resilience
cost_fnerrors never erase the model result. Ifcost_fn(result)raises or returns a non-Amount,CyclesModelGatelogs a warning and falls back to the configuredestimatefor the commit. The model result is still returned to the agent. This means a stale or wrong extractor downgrades the debit accuracy (estimate vs. actual) but never breaks the agent loop. Locked down bytests/test_model_gate.py::test_cost_fn_exception_falls_back_to_estimate,::test_cost_fn_invalid_return_falls_back_to_estimate, and the async siblings.
Coverage
141 tests, 99.59% coverage (gate fail_under = 95); model_gate.py and extractors.py both at 100%. Cost-fn coverage now includes applied / None-fallback / exception-fallback / invalid-return fallback / decide+reserve parity / not-called-in-decide-mode across sync and async paths, plus missing-reservation-id async coverage and awaitable-handler reserve-mode coverage on CyclesModelGate. Extractor tests cover OpenAI/Anthropic shape extraction, zero-token edge cases, missing-usage-metadata fallback, missing token fields, negative token counts, empty-result fallback, fractional-cent rounding, and the keyword-only pricing-arg guard.
Behavior change
cost_fn is purely additive. Callers on v0.1.x who don't pass it see no change. Callers who pass it get actual-cost commits instead of estimate-cost commits — a more accurate debit, never a denial path change.
Deferred to a later v0.2.x
- Streaming integration verification.
wrap_model_callruns once per turn even when the underlying call streams; we need explicit tests confirming the commit fires after the stream is fully consumed. Tracked separately on #13. examples/multi_agent_fanout.py— the multi-tenant fan-out + HITL demo agent that meets LangChain's stated co-marketing bar. Tracked separately on #13.
Assets 2
v0.1.6
2c0894f Doc cleanup pass. v0.1.5 shipped CyclesModelGate but several places in the repo still described the package as "two middleware classes" or "no model-call middleware yet." External review caught the staleness; this release brings the user-visible metadata in line with what the v0.1.5 code actually does. No code changes; behavior is identical to v0.1.5.
Fixed
pyproject.tomldescription now reads "pre-execution budget authority for model calls, tool calls, and runaway agent loops" (was "pre-tool-call authorization, fan-out caps, and per-tenant budget enforcement"). PyPI listing now correctly advertises the v0.1.5 surface.langchain_runcycles/__init__.pymodule docstring now describes threeAgentMiddlewaresubclasses (was two), with composition guidance (fan-out → model → tool ordering).- README "Known limitations": replaced "No model-call middleware yet" with the accurate "architecture-complete but commits at estimate" framing —
CyclesModelGateexists, just doesn't yet extract actual provider token usage. docs/runcycles.mdx: opening paragraph updated from "two classes" to "three classes"; Details metadata table now listsCyclesModelGatealongsideCyclesToolGateandCyclesFanOutGate; "v0.1.x scope" subsection rewritten to honestly distinguish architecture milestone (v0.1.5) from production polish (v0.2.0).AUDIT.mdAudit Scope section now lists model hooks (wrap_model_call/awrap_model_call); SDK methods table now referencesmodel_gate.pyalongsidetool_gate.pyfordecide/create_reservation/commit_reservation/release_reservation.
Behavior change
None. Doc cleanup only. v0.1.5 callers' code unchanged.
Assets 2
v0.1.5
759280f Adds CyclesModelGate — pre-model-call authorization middleware — closing the third leg of the LangChain agent governance triad. Closes #10.
CyclesToolGate already gated tool calls; CyclesFanOutGate already capped model turns; this release adds the third middleware so model calls themselves are intercepted via LangChain's wrap_model_call hook. With v0.1.5, the package can truthfully say it puts pre-execution authority in front of model calls, tool calls, and runaway agent loops.
This is the architecture milestone — feature parity with CyclesToolGate. The production milestone (v0.2.0) will add provider-specific token-cost extractors, streaming integration, and a polished demo agent.
Added
CyclesModelGate— newAgentMiddlewaresubclass overridingwrap_model_call(sync) andawrap_model_call(async). On denial indecidemode, returns aModelResponsewhoseAIMessagecarries the denial reason (the agent terminates naturally because the AIMessage has notool_calls).- All three modes (
decide/reserve/decide+reserve) at parity withCyclesToolGate. settlement_error_policyparity (default"raise").idempotency_namespaceparity (static or callable, callable receives theModelRequest).- Public API exports:
CyclesModelGatere-exported fromlangchain_runcycles.
Changed
- README "What's in the box" gains a third bullet for
CyclesModelGate. - AUDIT.md hooks table gains
wrap_model_call/awrap_model_callrows; "No model-call middleware yet" line removed from Known Limitations. docs/runcycles.mdxgets a third middleware section + a 3-class composition example.
Known limitations (carried into v0.2.0)
- Commits at the configured
estimate, not actual token cost. Provider-specific token extraction (OpenAI, Anthropic) is v0.2.0 scope. For precise per-call actual-cost capture today, use the callback handler fromcycles-client-pythoninstead, or until v0.2.0 ships. - No streaming integration. For streaming LLM calls, use
runcycles.stream_reservationdirectly. v0.2.0 may add streaming support insidewrap_model_call. - Per-call key uses UUID fallback. Model-call requests don't carry an upstream-stable id like
tool_call_id, so each call gets a fresh UUID for the per-call slot. Namespacing (model-decide-{namespace}-{32-hex}) still scopes by run/workflow/tenant. v0.2.0 may extract a turn-id or message-hash for full retry-stability.
Test coverage
- 28 new tests (115 total, was 87). Coverage 99.07% (gate 95%).
- Sync + async parity for all paths; dedicated tests for the deny-path
ModelResponseshape.
Backward compatibility
Purely additive. v0.1.4 callers' code unchanged.
Assets 2
v0.1.4
3855142 External review of v0.1.3 caught two real defects (stale README dep line, type alias narrower than the documented per-call opt-out) plus a docs-example issue (undefined helper in a runnable-looking snippet). All fixed; no functional change.
Fixed
- Stale
langchain-core >= 0.3line in README's Requirements section.pyproject.tomlwas correctly tightened to>=1.0,<2.0in v0.1.2 but the README wasn't updated at the time. Now consistent. IdempotencyNamespaceResolvertype alias widened fromCallable[[Any], str]toCallable[[Any], str | None]. Runtime already supported a callable returningNone(per-call opt-out, documented since v0.1.3 and tested bytest_callable_namespace_returning_none_opts_out_per_call). The type signature was narrower than the documented contract; strict-mypy users would have hit a false negative when writing the documented opt-out callable. No behavior change.- Docs idempotency-namespace example now uses
request.state["run_id"]instead of an undefinedcurrent_run_id()/current_run_id_contextvar.get(...)helper. The new form is runnable against a real LangChainToolCallRequestand matches the LangChain idiom for accessing per-call state. README,docs/runcycles.mdx, and the langchain-ai/docs PR mirror all updated together.
Behavior change
None. Type widening is permissive (the old narrower type was a strict subset of the new one), README and docs are user-facing prose. v0.1.3 callers' code keeps working unchanged.
Assets 2
v0.1.3
b2d0f6d Adds run / workflow / tenant scoping to Cycles idempotency keys, addressing the v0.1.2 review concern about cross-run collision when frameworks reuse short tool call ids like tc_1. Backward-compatible — keys without a configured namespace keep the v0.1.2 shape exactly. Closes #6.
Added
idempotency_namespaceconfig onCyclesToolGateandCyclesFanOutGate. Optionalstr | Callable[[Any], str]. When supplied, every Cycles idempotency key becomes{prefix}-{namespace}-{tool_call_id}(or{prefix}-{namespace}-{32-hex}in the fanout case where there's no per-call upstream id). The callable form receives the LangChainToolCallRequestfor tool gates and the agentstatefor fan-out gates — useful for extracting a workflow run id, tenant id, or other run-scoped context.make_idempotency_keynow accepts anamespacekeyword-only argument. Same shape semantics as the middleware-level config; useful when calling the helper directly from custom integrations.- Public type aliases
IdempotencyNamespaceandIdempotencyNamespaceResolverre-exported from the package root for users typing their config callables.
Changed
- AUDIT.md idempotency-key section updated with the new four-shape table (namespace+suffix, suffix only, namespace only, neither) and cross-references to the namespace tests.
- README gains an "Idempotency-key namespacing" subsection under Configuration showing static and callable forms with a practical run-id example.
- MDX Production notes mirrors the README addition (will land via the same content sync that keeps the langchain-ai/docs PR in step).
Backward compatibility
No behavior change for users not setting idempotency_namespace. Locked down by tests/test_tool_gate.py::test_no_namespace_preserves_v012_key_shape. New tests covering static, callable, fanout-state-derived, and cross-run-collision-prevention paths bring the suite to 85 tests, ≥99% coverage.
Assets 2
v0.1.2
0caa058 Credibility patch addressing external review feedback. Three real correctness/copy issues that landed in the v0.1.0/v0.1.1 cuts; nothing is co-marketed before these are fixed.
Changed
- README copy: removed
wrap_model_calloverclaim. Earlier copy listedwrap_model_callalongside the implemented hooks and said the package runs "before LLM calls or tool actions execute" — butwrap_model_callis not implemented in v0.1.x. The README now accurately scopes coverage towrap_tool_call(tool-call authorization) andbefore_model(fan-out caps), with an explicit note that model-call middleware is on the roadmap. - Brand: title and copy use "Cycles", not "Runcycles". "Cycles" is the product;
runcyclesis just the domain. H1 changed fromLangChain Runcycles — ...toCycles for LangChain — AI agent middleware for budget and action authority. Same fix indocs/runcycles.mdx(titleCycles middleware integration). - Copy: dropped the "Works with LangGraph, LangSmith, OpenAI, Anthropic, MCP servers..." framework name-drop. Replaced with
Provider-neutral: works with any LangChain 1.x agent regardless of model provider, as long as actions flow through LangChain middleware/tool execution.More accurate for a middleware package that's truly model-agnostic at this layer. - Tightened
langchain-coredep from>=0.3to>=1.0,<2.0per review feedback.langchain >=1.0,<2.0already pullslangchain-core 1.xtransitively; the explicit pin avoids a stale 0.x landing accidentally in mixed environments. - Settlement docs: added a retry-duplicate-side-effect callout. When
settlement_error_policy="raise"propagates the commit failure, a LangChain agent may retry — at which point the tool's side effect (email, payment, write) repeats. README and the MDX Production-notes section now flag this so users can choose"log"deliberately for non-idempotent tool side effects.
Fixed
- Idempotency keys are now deterministic per
tool_call_id. Previous shape was{prefix}-{tool_call_id}-{8-hex-uuid}, which made every retry of the same tool call land on a new Cycles reservation — defeating the point of idempotency. New shape is{prefix}-{tool_call_id}so a duplicate dispatch (durable workflow replay, middleware retry, process recovery) lands on the same reservation. Random UUIDs are used only as a last-resort fallback when the upstream omitstool_call_id. Locked down bytests/test_tool_gate.py::test_idempotency_keys_are_deterministic_per_tool_call_idand::test_idempotency_key_retry_lands_on_same_key.
Added
settlement_error_policyconfig onCyclesToolGate. NewLiteral["raise", "log"]parameter controlling what happens if the post-tool-runcommit_reservationcall itself fails. New default"raise"propagates the commit failure so the caller can reconcile (governance-first); the previous v0.1.0/v0.1.1 behavior is opt-in via"log". Documented tradeoff in README "Settlement (commit) failures" subsection. Tests:test_settlement_raise_default_propagates_commit_failure,test_settlement_log_swallows_commit_failure, plus async siblings andtest_invalid_settlement_policy_raises.
Behavior change (minor)
settlement_error_policy defaults to "raise". Users on v0.1.0/v0.1.1 who relied on the swallowed-commit-failure behavior should explicitly pass settlement_error_policy="log". Default chosen because for a governance package, silently dropping accounting on commit failure is more dangerous than surfacing the error.