Skip to main content

nodloop for Python

Record what your agent answered, take a person's verdict on it, and hand the corrections they approve to the agent's next run in the same situation. This is the Python client of nodloop.

pip install nodloop
pip install "nodloop[langchain]"      # or openai-agents, claude-agent

The client starts a local nodloop mcp over stdio, so no server runs. It uses NODLOOP_BIN, then the binary the Claude Code plugin keeps under ~/.nodloop/bin, then nodloop on PATH, and otherwise downloads the release of its own version from GitHub and checks it against the release checksums. It requires Python 3.10 or newer and is tested on Linux with Python 3.12.

The loop

from nodloop import Client

acme = {"tenant": ["acme"], "task": ["refund"]}

async with Client() as c:
    knowledge = await c.knowledge("support-bot", acme)
    prompt = f"{system_prompt}\n\n{knowledge.text}"  # empty text when nothing is approved
    answer = await my_agent(prompt)
    run = await c.record("support-bot", answer, labels=acme, applied=knowledge.items)

    # later, when a person reads the answer
    await c.judge(run, "reject", reason="the refund window was missing", reason_code="scope")
    candidate = await c.propose("judgment", "Quote the refund window before the steps", from_run=run)
    approval = await c.approve(candidate.id, candidate.version, approver="ann")

From then on every knowledge("support-bot", acme) carries the item, and a run of another tenant never does. judge takes approve, edit with edited= holding the corrected output, reject with what was wrong, or withdraw to take back the verdict before it.

Method Returns
knowledge(producer, labels, holdout_key) Knowledge with items, the text to put in the prompt and withheld. With a holdout_key such as <service>:<hour>, the holdout share of config.json gets no items and withheld instead
record(producer, output, *, labels, applied, subject, withheld, holdout_key) the run id a verdict cites. Pass knowledge.withheld as withheld and the holdout_key the run passed to knowledge. report effect counts only runs recorded with the key and counts a run without it as undrawn. withheld takes Ref or Item values
judge(run, verdict, reason, reason_code, edited, reviewer) nothing
propose(kind, content, from_run, producer, labels, new_labels, item_id, veto) Candidate with id, version, status and overlaps, the current items of the same kind one run may carry with it
approve(item_id, version, approver, labels=) Approves as proposed, or for the runs carrying labels when given, and {} for every run of the producer. Approval with id, version, status, approver, veto, compaction_due when one run would carry more than five items, and export_error and folder_error when a step after the approval failed
check_call(tool, arguments, producer) Decision with action of allow, block or ask, veto and reason
outcome(run, result, confirmed_cause, note, reviewer) nothing. result is a Result, confirmed, refuted or inconclusive: what a real check found, apart from the verdict on the output
report(name) Report with report, the report as JSON, and since, the RFC3339 start of the runs it read or empty for every run. name is a ReportName: loop, extract, critic, effect, health or replay

A tool that refuses, such as a proposal from a run nobody corrected, raises ToolError with the reason.

Every adapter takes an optional holdout_key, passes it to knowledge and record and records what came back as applied or withheld.

A stream producer with a holdout

A producer that runs on a schedule, such as an anomaly check over each window of a stream, draws the holdout on a key it keeps for the run. Use <service>:<time block of an hour or more> so the windows of one block land in the same arm and a lesson seen in one window does not leak into a withheld window next to it.

from datetime import datetime, timezone

block = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H")  # one hour
orders = {"service": ["orders"]}
knowledge = await c.knowledge("lag-check", orders, holdout_key=f"orders:{block}")
answer = await check(window, knowledge.text)
run = await c.record(
    "lag-check", answer, labels=orders, applied=knowledge.items, withheld=knowledge.withheld, holdout_key=f"orders:{block}"
)

# later, when the cause was checked
await c.outcome(run, "confirmed", confirmed_cause="partition 7 was hot")
effect = (await c.report("effect")).report

The share comes from holdout in config.json. With url the share is the one in the config.json of the nodloop server, read when the server starts.

Adapters

LangChain create_agent middleware fetches the items once per run and puts them in the system message, records the final answer and refuses a vetoed tool call:

from nodloop.langchain import NodloopMiddleware

mw = NodloopMiddleware(c, "support-bot", acme)
agent = create_agent(model, tools, middleware=[mw])
await agent.ainvoke({"messages": [...]})
await c.judge(mw.run, "approve")

OpenAI Agents SDK hooks and a tool input guardrail:

from nodloop.openai_agents import NodloopHooks, veto_guardrail

hooks = NodloopHooks(c, "support-bot", acme)
agent = Agent(name="support", instructions=hooks.instructions("You help with refunds"), tools=[...])
await Runner.run(agent, "refund?", hooks=hooks)

Pass veto_guardrail(c, "support-bot") in a tool's tool_input_guardrails.

Claude Agent SDK hooks, the same loop the Claude Code plugin runs:

from nodloop.claude_agent import NodloopHooks

hooks = NodloopHooks(c, "support-bot", acme, holdout_key=conversation_id)
options = ClaudeAgentOptions(hooks=hooks.hooks())

Use one adapter instance per run or conversation, since it keeps the items it handed out and the id of the run it recorded.

The Claude Agent hooks draw once per conversation since the key stays the same for every turn, so a whole conversation is in one arm. report effect counts every turn of one key as one draw unit, so the turns of a conversation widen its intervals instead of passing for independent samples.

A shared server

Client(url="https://nodloop.example/mcp", key="nl_...") connects to nodloop-server serve instead of a local binary. The key decides the tenant and what the client may do, and an approval through it is recorded under the key's name.

More

The nodloop README covers labels, vetoes, reports and the server. MIT licensed.

Metadata

Release files for nodloop 0.9.0

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

Source distribution (sdist)

Source distribution for nodloop 0.9.0
File Size Uploaded
nodloop-0.9.0.tar.gz 177.4 kB Details

Built distribution (wheel)

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

Total release size: 191.0 kB

Release files / nodloop-0.9.0.tar.gz

Download URL nodloop-0.9.0.tar.gz
Size 177.4 kB
Tags Source
SHA-256 checksum
How to use checksums
47c9fb328fa388392e22e46573c4a50e6432ed78fe716f667f57758d1be9e56a
BLAKE2b-256 checksum
How to use checksums
d8c7b77145b6010b94ff300fd304e8ba6b53cd412514bbfd638141926f7d6801
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / nodloop-0.9.0-py3-none-any.whl

Download URL nodloop-0.9.0-py3-none-any.whl
Size 13.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c93c21d16b526d49513689d931516da181bbed27978b42aa485b957bfc375fb7
BLAKE2b-256 checksum
How to use checksums
05b524d6b74e5b33670ebe720804e774e60963a4642333dd09b45de956bb9799
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.9.0 This release

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.6

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