doubleoh
Your agents learn from every fix. Stop failing the same way twice.
When your agent gets stuck on a real website, one of your people fixes it once in a live browser. That fix compiles into a skill. The next time any agent in your fleet hits the same wall, it follows the skill instead of asking a human.
pip install doubleoh
Zero dependencies — it is urllib and dataclasses.
The loop, in code
from doubleoh import DoubleOh
doubleoh = DoubleOh(api_key=os.environ["DOUBLEOH_API_KEY"])
# 1. Before your agent tries something it has failed at before, ask what has been learned.
skills = doubleoh.skills_for("check out on the supplier portal")
if skills and skills[0].times_worked > skills[0].times_failed:
prompt += f"\n\nA colleague has done this before:\n{skills[0].instructions}"
# 2. Your agent runs. When it gets stuck, ask for a human.
fix = doubleoh.request_fix(url=page.url, task="Check out on the supplier portal")
if fix.deflected:
# Nobody was paged: the fleet already knows this wall. Retry with the skill.
prompt += fix.skill.instructions
elif fix.fix_url:
slack.send(f"An agent needs a hand: {fix.fix_url}")
# fix.duplicate_of set? The wall is already waiting on a human — fix.blocked_runs says
# how many runs are stuck on it. Your retry loop needs no change; nobody gets re-paged.
# 3. Close the loop: say whether the skill worked. This is what makes the counts mean
# something, and a skill followed three times that never works retires itself.
doubleoh.report_skill(skills[0].name, worked=True)
You decide what "stuck" means — a timeout, a retry count, a model that says it cannot proceed. Only you know your agent.
API
skills_for(task) -> list[LearnedSkill]— what has been learned; putinstructionsin your agent's context.times_worked/times_failedare raw counts on purpose: 5 for 5 is worth following, 1 for 1 is a guess that happened to work once.request_fix(url, task) -> Intervention— ask for help. Three possible shapes: afix_url(a human was paged),deflected=Truewithskill(nobody was paged — you already know the answer), orduplicate_ofwithblocked_runs(already waiting; triage by the count).urlmust be a public page; private and loopback addresses are refused.report_skill(name, worked)— one boolean; wire it to whatever already tells you a run succeeded.intervention(id)/interventions()— status;skill_nameis the receipt.retire_skill(name, reason)— stop serving a skill immediately.
Errors raise DoubleOhError carrying the server's own message and the HTTP status.
What is recorded
Fix sessions keep screenshots and the shape of each action. Typed text is never recorded — a fix session is exactly where a password gets typed, so keystrokes stay off the record entirely.
Framework adapters
Every adapter serves the same three tools; pick your framework's line.
from doubleoh import DoubleOh
client = DoubleOh(api_key=os.environ["DOUBLEOH_API_KEY"])
# LangChain / LangGraph
from doubleoh.langchain import doubleoh_tools
graph = create_react_agent(model, tools=doubleoh_tools(client))
# CrewAI
from doubleoh.crewai import doubleoh_tools
agent = Agent(role="operator", tools=doubleoh_tools(client))
# LlamaIndex
from doubleoh.llamaindex import doubleoh_tools
agent = ReActAgent.from_tools(doubleoh_tools(client), llm=llm)
# OpenAI Agents SDK, AutoGen, Pydantic AI — all accept plain functions:
from doubleoh.tools import build_tools
tools = build_tools(client) # skills_for, request_fix, wait_for_fix, report_skill, check, open, confirm
agent = Agent(tools=[function_tool(f) for f in tools])
No framework is a dependency of this package: each adapter imports its framework lazily
and, if it is missing, raises one sentence naming what to pip install.
Using Claude or another MCP-capable agent? Skip the SDK entirely — doubleoh-mcp serves
the same tools over MCP.
Metadata
Release files for doubleoh 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| doubleoh-0.2.1.tar.gz | 20.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| doubleoh-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.5 kB
Release files / doubleoh-0.2.1.tar.gz
| Download URL | doubleoh-0.2.1.tar.gz |
|---|---|
| Size | 20.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f971f248698dc1194e8f1f3d06648ed8f3b80bdf93ac450085a03812b0b379d9
|
|
BLAKE2b-256 checksum How to use checksums |
574dc4e463a5d6ee3eee0a5a375e07c7a07f2eeab37bb12e559278c5a8415044
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / doubleoh-0.2.1-py3-none-any.whl
| Download URL | doubleoh-0.2.1-py3-none-any.whl |
|---|---|
| Size | 16.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9eea01d30d918bccf1e6e9485f90b26e176d0ab46e2c7a2fcd1d37e4407e7902
|
|
BLAKE2b-256 checksum How to use checksums |
2a7e8dccc7844f3a08485b7b7c2ec150381d930152ded0c5cbfb7b2a703e852f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for doubleoh 0.2.1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.