Skip to main content

evenkeel

Keeps a LangChain agent on an even keel when things fail: failures are classified and handled by kind, bad output is retried with a hint, spend is tracked in dollars with a budget, and failures that need a person pause the run for approval.

Ported from keel, a durable execution engine for AI agents. An independent project, not affiliated with LangChain. Experimental: 0.x, expect changes.

What it adds to LangChain's built-in middleware

Need Built in EvenKeelMiddleware
Retries ModelRetryMiddleware, ToolRetryMiddleware retry with the same input Classifies the failure first: back off, retry with a hint, fail at once, or escalate
Limits ModelCallLimitMiddleware counts calls Tracks dollars from token usage and pauses at a budget
Approvals HumanInTheLoopMiddleware asks before chosen tools Escalates when a failure needs a person, from any model or tool call

Use it alongside the built-ins: ModelFallbackMiddleware for switching models, HumanInTheLoopMiddleware for approvals fixed in advance.

Install

pip install evenkeel          # rule-based classification
pip install "evenkeel[jev]"   # adds TypeSafe's Jev classifier

Use

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from evenkeel import EvenKeelMiddleware, ModelPrice, PriceTable

agent = create_agent(
    "openai:gpt-4o-mini",
    tools=[...],
    middleware=[
        EvenKeelMiddleware(
            budget_usd=0.50,
            prices=PriceTable({"gpt-4o-mini": ModelPrice(input_per_mtok=0.15, output_per_mtok=0.60)}),
        )
    ],
    checkpointer=InMemorySaver(),  # needed for pauses
)

With Jev (needs TYPESAFE_API_KEY):

from typesafe_sdk import TypeSafeClient
from evenkeel import EvenKeelMiddleware, JevClassifier

EvenKeelMiddleware(classifier=JevClassifier(TypeSafeClient()))

How failures are handled

Kind Example Action
transient 429, 503, timeout, dropped connection Back off and retry the same call
bad_output Malformed output, a call that breaks the schema (recognised by Jev, or raised as BadOutputError) Retry with a hint saying what was wrong
bad_input Input a tool rejects (BadInputError, HTTP 400 or 422, a pydantic ValidationError) Model call: fail. Tool call: return the error to the model so it fixes its arguments
needs_human Refund over a limit, a policy block Pause with interrupt() and wait for a decision
fatal A bug, bad credentials Re-raise the original error at once

Retries and escalations stop at max_attempts (default 4). Each failure is recorded (error, kind, confidence, which classifier decided, action): in the agent's state under evenkeel_attempts when the call ends in a result, and on the raised error as error.evenkeel_attempts when it finally fails.

Some failures never reach the middleware, because LangChain turns them into messages for the model itself: a call to a tool that does not exist, and tool arguments that fail the tool's schema.

Pauses and how to resume

A failure that needs a person:

from langgraph.types import Command

agent.invoke(Command(resume={"decision": "retry", "hint": "approved by finance"}), config)
agent.invoke(Command(resume={"decision": "fail"}), config)
agent.invoke(Command(resume={"decision": "skip", "message": "Refunded by hand."}), config)  # tool calls only

A retry with a hint on a tool call sends the reviewer's words to the model, which can call the tool again; a tool cannot take a hint itself. A call that still needs a person after one approved retry fails. An answer that is not one of these values does not raise: the run pauses again, with an error field in the payload saying what was wrong.

Over budget:

agent.invoke(Command(resume={"approve": True, "budget_usd": 1.00}), config)
agent.invoke(Command(resume={"approve": False}), config)  # ends the run

The budget applies to each run: every new invoke on a thread starts with zero spend and the configured budget.

Limitations

  • Budgets can overshoot by one call. The check runs before a model call, but a call's cost is known only after it. keel has the same limitation.
  • No built-in prices. You supply them; a model without a price, or a response without token usage, is an error rather than a silent $0. Dated or versioned names (gpt-4o-mini-2024-07-18, claude-3-5-sonnet-20241022, gpt-4-0613, ...-latest) use the price of their base name; any other variant (gpt-4o-mini against gpt-4, gpt-4-turbo) needs its own entry.
  • On resume after an escalation, the failing call runs once more before the decision applies, because LangGraph re-runs the paused node. For a tool with side effects, make it idempotent or use skip.
  • Classification is only as good as the classifier. Rules only recognise explicit signals (status codes, network errors, evenkeel's error classes): on keel's 75 error messages from public SDK issues they pick the right kind 29 times and the right action 39 times. With Jev the results are below.

Classifier accuracy

Measured with evals/jev_eval.py on keel's labelled error sets: 75 error messages from public SDK issues and 60 sampled from 13 other repositories. Jev pinned to jev-1.13.0, one run on 2026-10-14. "Right action" counts a verdict as right when the default policy does the same thing as for the label (bad_input and fatal both fail).

Classifier Public set, right kind Public set, right action Fresh set, right kind Fresh set, right action
Rules only 29 / 75 (39%) 39 / 75 (52%) 43 / 60 (72%) 48 / 60 (80%)
JevClassifier (rules for explicit signals, Jev for the rest, rules below 0.5 confidence) 50 / 75 (67%) 63 / 75 (84%) 49 / 60 (82%) 56 / 60 (93%)

Caveats: the labels are one person's (keel's author, who also wrote the classifier prompt), most messages come from development rather than production, and Jev's answers vary by about one case between runs. On the public set Jev answered 66 cases and the rules 9 (Jev below 0.5 confidence), of which 7 were wrong. keel's TypeScript cascade scored 46 to 47 and 59 to 60 on the same set (its results); the difference is in how often Jev falls below the confidence floor, and its cause has not been established.

Develop

uv sync --all-extras
uv run pytest

Metadata

Release files for evenkeel 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for evenkeel 0.1.1
File Size Uploaded
evenkeel-0.1.1.tar.gz 26.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for evenkeel 0.1.1
File Interpreter ABI Platform
evenkeel-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 43.7 kB

Release files / evenkeel-0.1.1.tar.gz

Download URL evenkeel-0.1.1.tar.gz
Size 26.1 kB
Tags Source
SHA-256 checksum
How to use checksums
90befdc2da65d8558da709e4827c6b1b2ee997df35f787f45f243c08648ab86f
BLAKE2b-256 checksum
How to use checksums
9a4c19ef05f7a8058b5ace640054b7102031ae20739b74337478b072ab6a58a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / evenkeel-0.1.1-py3-none-any.whl

Download URL evenkeel-0.1.1-py3-none-any.whl
Size 17.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8dc466b8df1d864782a1497c5ec81e29cee2b6b1e5b2d490e5d5d9e757a71bb
BLAKE2b-256 checksum
How to use checksums
7f444b53f3415e813675f40630f947776ef87af5d351aa87347254d26da82e3b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page