agent-feedback
Tell your agent what it did wrong in less than 10 lines. A zero-dependency, framework-agnostic Python harness for LLM application validation and stateful feedback retries.
pip install agent-feedback
from agent_feedback import RetryableFailure, arun
def require_tool_call(response):
if not response.tool_calls:
raise RetryableFailure(
"Model did not call a tool.",
feedback="You must call a tool for this task.",
)
result = await arun(
request=messages,
invoke=llm_invoke,
validators=[require_tool_call],
apply_feedback=lambda feedback, previous_request: previous_request + [
{"role": "system", "content": feedback}
],
max_attempts=3,
)
That's the whole idea: invoke, inspect, give feedback, retry.
agent-feedback is a small, framework-agnostic feedback loop around any LLM invocation callable - that llm_invoke is YOUR invokable.
It doesn't replace your model SDK or agent framework. It doesn't know or care about invocation shapes. You define those shapes; the harness just runs the loop.
Why?
Because LLM output can be valid and still be wrong.
A schema can tell you that this is a valid tool call:
{
"origin": "NYC",
"destination": "NYC",
}
It can't tell you that the user probably didn't mean to search for a flight from a city to itself.
That's application-level validation:
def validate_flight_search(tool_call):
if tool_call.name != "search_flights":
raise RetryableFailure(
"Wrong tool selected.",
feedback="Use the search_flights tool for this request.",
)
if tool_call.input["origin"] == tool_call.input["destination"]:
raise RetryableFailure(
"Origin and destination are identical.",
feedback="Choose a destination different from the origin.",
)
The feedback becomes context for the next attempt.
The mental model
request
|
v
invoke
|
v
extract (optional)
|
v
validators
|
+-------> return None ---------> done
|
v
raise RetryableFailure
|
v
apply_feedback
|
v
request -------------> (back to top)
To remember the pipeline, note the stages map directly to the arun() parameters, read top-to-bottom:
request— The argument or arguments passed to your invocation callable.invoke— The callable performing the actual execution/LLM call.extract(optional) — Post-processes the raw response into the target shape for validation.validators— A list of functions that evaluate the extracted output.apply_feedback— Dictates how the feedback transforms the original request for the next attempt.
The output of one function flows naturally as the input to the next.
For a step-by-step guide, see Getting Started.
Built to be spammable, no matter your existing SDK
There's no adapter layer, so there's almost no integration cost to wrapping every invoke() callsite in your app, not just the risky ones.
Your extractor and validators naturally adapt to whatever shape each SDK hands back — Anthropic's .content blocks, LangChain's .tool_calls, whatever. The core loop stays the same even when the SDK does not:
from agent_feedback import arun, Request
# Anthropic — Messages API
await arun(
request=Request(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages,
),
invoke=client.messages.create,
validators=[require_tool_call],
)
# OpenAI — Responses API
await arun(
request=Request(
model="gpt-5.5",
input=messages,
),
invoke=client.responses.create,
validators=[require_tool_call],
)
# LangChain — any BaseChatModel
await arun(
request=messages,
invoke=model.ainvoke,
validators=[require_tool_call],
)
# Combine with Instructor
instructor_client = instructor.from_provider("openai/gpt-4o-mini")
await arun(
request=Request(
response_model=UserBaseModel,
messages=[{"role": "user", "content": "John is 250 years old"}],
),
invoke=instructor_client.chat.completions.create,
validators=[user_age_reasonable], # your own validator
)
Request bundles
argsandkwargsfor your invoke callable. It's unnecessary when your invoke takes a single argument. Inapply_feedback, access them viaprevious_request.argsandprevious_request.kwargs.
This cheapness compounds:
- Reuse one pipeline everywhere. Build it once with Runner, then call it from every site that shares the same
invoke/extract/apply_feedback. - Raise
RetryableFailurefrom anywhere. Not just validators — raise it insideinvokeif a provider call produces a recoverable failure, or insideextractif parsing fails. The same loop handles it. - Use validators as deterministic evals. A
validatoressentially checks the correctness of an LLM output - which is a free eval. Attach one that records the output and returnsNone, and you can collect pass/fail or quality telemetry at every call site without changing your invocation code.
Why not just a while loop?
for attempt in range(3):
response = await llm_invoke(request)
try:
validate(response)
return response
except RetryableFailure as e:
request = apply_feedback(e.feedback, request)
You can. That's essentially the core of agent-feedback.
The value isn't in hiding a complicated algorithm. It's in providing a reusable abstraction around a pattern that tends to grow as your application needs more: extraction, multiple validators, structured failures, attempt history, exhaustion handling, and consistent retry behavior.
If a five-line loop is all you need, write the five-line loop.
If you're writing the same loop repeatedly, use agent-feedback.
Scope
v0.1 is deliberately small:
- Async-first.
- No streaming support.
- No batch orchestration.
- No provider-specific adapters in core.
- No automatic inference about how requests should change.
The package is a loop around your callable, not another agent framework. If you need any of these features, you can either build your own abstractions or raise an issue. The API is designed to be extensible.
Today, agent-feedback is stdlib-only. Zero-dependency. We will attempt to keep it that way for the foreseeable future.
Further reading
- Getting Started — first successful
arun(...)loop, extraction, validation, and feedback. - API Reference — exact signatures, callback shapes, failure semantics, and history/result details.
- Patterns —
Runnerreuse, provider-shaped feedback, decorator recipes, and observability patterns.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_feedback-0.1.0.tar.gz.
File metadata
- Download URL: agent_feedback-0.1.0.tar.gz
- Upload date:
- Size: 18.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8de4e98f75c9946c726b94861c13224fac707bfc69561040406c0ab1b7ee2ff4
|
|
| MD5 |
2a4b2ff928099ab2f635d3c5e47aff37
|
|
| BLAKE2b-256 |
cb983874d9817c39aa9804c9b4def3ad4ee9d3177103cc8dac21089f9a990f27
|
File details
Details for the file agent_feedback-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agent_feedback-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0955cab65c609a97e4364b6295f02673a6a5d259c387f107992efad9243ce638
|
|
| MD5 |
c47c8e79d3ef53fa568a424ea8f90a99
|
|
| BLAKE2b-256 |
930819deaa13521dee1586a5a9d6c9cc2167fb0271704b0e4618a585a6718cb5
|