able-harness-kit
Thin, backend-neutral agent-harness middlewares for LangChain / deepagents.
Production-distilled patterns that compose on top of any LangChain
AgentMiddleware stack (including deepagents).
They don't replace your agent — they harden the loop around it.
pip install able-harness-kit # after the first PyPI release
# or, from a clone:
pip install -e .
Why
Most agent failures aren't "the model isn't smart enough" — they're the loop around the model lacking observation and control:
| Middleware | The failure it kills |
|---|---|
ProgressAwareLoopGuardMiddleware |
The agent repeats a tool call that returns no new information, spinning until it burns its budget — while not tripping on polling or pagination, which repeat the call but get new results. |
LoopGuardMiddleware |
The cheaper case: the agent calls the same tool with identical args N times in a row (no view of the result needed). |
BinaryReadGuardMiddleware |
read_file hands the model a base64 block for a binary file; a lossy gateway drops it; the model claims it read the file and hallucinates. |
ToolResultBudgetMiddleware |
A 200 KB tool dump blows the context window and forces premature summarization. |
Each is backend-neutral (depends only on LangChain / LangGraph public types), small enough to read in one sitting, and composes with — rather than replaces — your existing harness.
Usage
from langchain.agents import create_agent # or deepagents.create_deep_agent
from able_harness_kit import (
ProgressAwareLoopGuardMiddleware,
BinaryReadGuardMiddleware,
ToolResultBudgetMiddleware,
)
agent = create_agent(
model,
tools=[...],
middleware=[
BinaryReadGuardMiddleware(), # fail loud on binary read_file
ToolResultBudgetMiddleware(limit=16_000), # cap oversized tool results
ProgressAwareLoopGuardMiddleware(stop_at=3), # stop loops that make no progress
],
)
ProgressAwareLoopGuardMiddleware
The cheap loop signal — "same tool, same args, N times" — is wrong about half the time. Polling a job, walking pagination, or tailing a stream all repeat the call and should: each one returns new information. The difference between progress and a stuck loop isn't in the arguments, it's in the output.
This guard runs the tool, compares the result to the previous same-tool result,
and intervenes only when a call both repeats and returns no new information
(same_args_repeat_count >= stop_at and new_information_delta < progress_floor).
Genuine progress passes through; a true spin gets a model-facing directive (not a
synthetic user message). For tools where a content-hash comparison is too lax — a
TODO tool re-sent with a one-character cosmetic edit, say — pass
delta_overrides={"write_todos": make_set_membership_delta("todos")}.
The output-delta primitives are exported standalone:
build_observations(state["messages"]) reconstructs the per-call observations
(ToolCallObservation with new_information_delta) from history with no instance
state.
LoopGuardMiddleware
The cheaper variant: fingerprints each call (name + normalized args), counts
consecutive identical-argument repeats, and short-circuits once stop_at is hit —
without looking at the result. Use it when you only want to catch a model hammering
an identical call and don't need output tracking. The counter resets the moment a
different call is seen. Pass on_signal= to observe without changing control flow.
BinaryReadGuardMiddleware
Intercepts read_file results whose declared media type isn't text-like and
replaces the base64 payload with a structured error pointing at a dedicated
extractor (OCR / doc-to-markdown / type sniff). Turning a silent hallucination
into an explicit "use the right tool" nudge is strictly safer; the only cost is
one extra tool hop on the first attempt. Tool name, metadata keys, and the
directive are configurable; disable per-deployment with
BINARY_READ_GUARD_ENABLED=0.
ToolResultBudgetMiddleware
Caps each tool result at a character budget. Oversized results are offloaded via
a caller-supplied offload(content, tool_name) -> OffloadRef callback (bring
your own store — filesystem, blob, vector DB), or truncated with a retrieval
hint by default.
Background
These were distilled from a multi-agent product running on non-Anthropic model gateways (Qwen / GLM / DeepSeek), and from a controlled experiment across 5 models × 9 behavioral probes (harness on vs. off) that framed a harness as an entropy-reducing control system rather than overhead. The progress-aware loop signal — keying off whether a repeated call returned new information, not just whether the arguments repeated — is the open distillation of that controller's loop detector. The middlewares here are the reusable, framework-neutral core of that work.
License
MIT © Hanlin Li
Release files for able-harness-kit 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| able_harness_kit-0.2.0.tar.gz | 20.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| able_harness_kit-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:37.7 kB
Release files / able_harness_kit-0.2.0.tar.gz
| Download URL | able_harness_kit-0.2.0.tar.gz |
|---|---|
| Size | 20.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3d54226c98d47335388e5b1cbc762b3149e31ea74475cc6b6030fc8975b79482
|
|
BLAKE2b-256 checksum How to use checksums |
9e8651a274c1a9089339823232a42dbf1ca2938526c396b9f9c2eed96bcd9a36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / able_harness_kit-0.2.0-py3-none-any.whl
| Download URL | able_harness_kit-0.2.0-py3-none-any.whl |
|---|---|
| Size | 17.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f35047471bee97568a3fc82d898a95ef1baa130113d15d86e4d561e44ef523cd
|
|
BLAKE2b-256 checksum How to use checksums |
07b92981e9c0e7437f1cc7b86468519d1033ed983e5f7add429479ccaa5048c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|