Catalog PyPI
PACKAGE EXPLORERView in registry

ahn-client

PyPI

Ask Agent Help Network what a host declares and recommends when your browser automation is blocked. Read-only, no dependencies.

agentsbrowser-automationbrowser-useclaude-agent-sdkcrewailangchainllms-txtopenai-agentsplaywrightrobots-txtweb-access
Versions collected1
Direct dependencies4From release 0.3.0
Last observed21 Oct, 09:39 UTC

README

Version 0.3.0 documentation

ahn-client

A small Python client for Agent Help Network: when your browser automation (Playwright, Browser Use, Stagehand-style scripts, plain HTTP) is blocked, ask what the host declares agents may do (its own robots.txt and llms.txt), where its official developer path is, what the provider's status feed says, and get one recommended action.

Agent Help Network is an independent project. It is not an official service of, and is not affiliated with, OpenAI, Meta, Muse, dots, Anthropic, or any site it describes. ahn-client is read-only (it has no reporting function), has no dependencies beyond the Python standard library, and runs on Python 3.9+. The optional framework adapters need the framework they plug into, and with it that framework's Python version (at the versions tested, all four need Python 3.10 or newer, and Browser Use 3.11).

Install

pip install ahn-client                       # no dependencies
pip install "ahn-client[langchain]"          # or [crewai], [openai-agents], [claude-agent-sdk]: see Framework adapters

Use

import ahn_client as ahn
answer = ahn.check("https://example.com/some/page", *ahn.classify("403 Forbidden"))
print(answer.summary() if answer else "no answer")

In an except block:

try:
    page.goto(url)
except Exception as exc:
    error_class, stage = ahn.classify(exc)
    if error_class:  # None: a 404 or a bad request, not a block or an outage
        answer = ahn.check(url, error_class, stage)   # a hostname and two codes leave your machine
        if answer:
            print(answer.recommendation, answer.official_url)

Examples with a real except block and a decision per recommendation: examples/playwright_example.py, examples/browser_use_example.py. Neither tries to get around a site's controls: they use the official path, hand off to the user, wait, or stop.

Framework adapters

When a web tool fails inside an agent framework with a block or an outage (403, 429, a challenge, a login wall, 5xx, a timeout, a refused connection), an adapter reads the hostname of the call's URL, looks it up with classify() and check(), and adds one line (Result.summary(), at most 700 characters) to what the agent sees. Each adapter is an extra: the base install has no dependencies and imports no framework.

All four follow the same rules:

  • They use only the extension point the framework's own documentation gives for a failed tool call.
  • The original error is not hidden or changed (LangChain's optional convert_exceptions=True is the one stated exception), nothing is retried, and no workaround is tried. A failure that is not a block or an outage (404, 422, a typo in an address), a call without an http(s) URL, an excluded host, a failed lookup or one slower than the time limit (2 seconds) adds nothing, and the run goes on exactly as it would have.
  • Only the hostname and two enum codes are sent (see What it sends). The error text, URL path and query stay on your machine.
  • The hostname comes from an http(s) URL in the call's arguments, or from the request the exception carries (httpx, requests, aiohttp, urllib). The text of an error is never searched for URLs, so an exception that only names a URL in its message, with no URL argument and no request, is not looked up (a message also links to help pages such as errors.pydantic.dev or docs.mistral.ai). The Claude Agent SDK hook is the exception: it reads its payloads by the Claude Code plugin's rules, which include the URL of a failed WebFetch. Tool-argument validation errors and errors with nothing web-like in them (a PermissionError, "Task blocked on mutex") are not looked up. Cluster-internal names (.svc, .consul, .cluster), address-embedding names (10-0-0-1.sslip.io), internationalized names that IDNA 2003 and 2008 read differently (faß.de) and URLs with a backslash are never looked up.
  • A tool has to fail for the adapter to see it. A tool that returns the text of a 403 page as an ordinary result is not a failure to the framework; make it raise (response.raise_for_status()) or report an error.
  • The same host is answered from the in-memory cache for 10 minutes, and each failing call gets the line again.

LangChain v1: wrap_tool_call middleware

from langchain.agents import create_agent
from ahn_client.langchain import ahn_tool_failure_middleware   # pip install "ahn-client[langchain]"

agent = create_agent(model, tools=[fetch_page], middleware=[ahn_tool_failure_middleware()])

Implements the documented wrap_tool_call / awrap_tool_call hooks.

  • A tool call that comes back as a ToolMessage with status="error" (a ToolNode with handle_tool_errors, ToolRetryMiddleware after its last attempt, or a tool that returns one) gets the line appended; the original content stays in front.
  • A tool that raises is left alone: the exception is re-raised unchanged, so the run behaves exactly as it does without the middleware whether or not Agent Help Network answers. An exception cannot carry text to the model.
  • ahn_tool_failure_middleware(convert_exceptions=True) also handles raised blocks and outages, always the same way and whatever the lookup returned: the exception becomes a ToolMessage(status="error") whose content is Error: <exception class> (<error class>) and the line when there is one. The exception's own text, URL, path and query are not put in the message. The run then goes on instead of stopping, which is why it is off by default.
  • If you already use ToolRetryMiddleware, list this middleware before it (the first middleware listed is the outermost): middleware=[ahn_tool_failure_middleware(), ToolRetryMiddleware()]. Retries then run first and this middleware sees the final failure once. After it, with convert_exceptions=True, the first exception would become a message and the retries would never run.
  • Not our behaviour, but worth knowing: ToolRetryMiddleware's default failure message (on_failure="continue") puts the exception's own text, including a URL with its query, into the message the model reads. This adapter only appends its line to it; use on_failure="error" or a function of your own there if the text should not reach the model.

CrewAI: ToolFailureDetectedEvent

from ahn_client.crewai import register_listener   # pip install "ahn-client[crewai]"

listener = register_listener(on_note=print)        # once, before crew.kickoff(); also listener.notes and the "ahn_client.crewai" logger

Subscribes to ToolFailureDetectedEvent on crewai_event_bus. CrewAI event listeners only observe: the event arrives after the agent's text is built, so the line goes to your callback, the log and listener.notes, not into the prompt. To append it to the tool result too, register ahn_client.crewai.append_note_to_result as an after_tool_call hook; it sees tools that return a ToolFailure, not exceptions.

OpenAI Agents SDK: RunConfig.tool_error_formatter

from agents import RunConfig, Runner
from ahn_client.openai_agents import ahn_tool_error_formatter   # pip install "ahn-client[openai-agents]"   (0.24 or newer)

result = await Runner.run(agent, "...", run_config=RunConfig(tool_error_formatter=ahn_tool_error_formatter))

Uses RunConfig.tool_error_formatter: for a function tool that raised, the SDK calls it with kind="tool_exception", the exception as error, default_message and run_context.tool_arguments. (The docs page lists the approval and tool-not-found kinds; the tool_exception kind is in the 0.24 source and its type annotations.) The formatter is async, so a lookup never blocks the event loop. It returns the SDK's default_message (the SDK's default does not show the model the exception text, and that stays) and the line on a second line; with a formatter of your own, chain it: tool_error_formatter(your_formatter).

  • It applies to function tools that use the SDK's implicit default failure handler. A tool with its own failure_error_function, tools made by Agent.as_tool() and local MCP tools keep their own policies. For a tool with its own function, wrap it: failure_error_function=with_ahn_note(your_function) (async by default; asynchronous=False gives the plain form the docs show, which waits up to 2 seconds on the event loop thread when the failure is a block or outage that is not cached).
  • Be aware of two SDK behaviours for tools that set their own failure_error_function, whichever function it is: the SDK then no longer applies RunConfig.tool_error_formatter to that tool, and a handled error is recorded in the trace span with its text instead of "redacted". The RunConfig formatter above keeps the SDK's default handling, so traces stay as they are.

Claude Agent SDK (Python): PostToolUseFailure hook

from claude_agent_sdk import ClaudeAgentOptions
from ahn_client.claude_agent_sdk import ahn_hooks   # pip install "ahn-client[claude-agent-sdk]"

options = ClaudeAgentOptions(hooks=ahn_hooks())      # or ahn_hooks(my_existing_hooks) to merge

Registers a PostToolUseFailure callback (claude-agent-sdk 0.1.27 or newer; that version and the current one were run) that returns the line as additionalContext. Which failures are looked up (WebFetch, and MCP browser or fetch tools whose input or error names an http(s) URL) is the Claude Code plugin's rule, which the adapter shares; a test runs both on the same payloads. Because WebFetch reports an HTTP 401, 403, 429 or 5xx as a successful call, ahn_hooks() also registers the plugin's PostToolUse case for WebFetch.

Browser Use: example

examples/browser_use_actions_example.py adds a custom action the model calls on a block page (@tools.action) and an on_step_end hook that reports a block page's answer to the developer. Browser Use documents no way for a hook to add text to the model's context, so only the action reaches the model.

Each adapter names itself in the User-Agent (see below). AHN_CLIENT_DISABLE=1 and AHN_CLIENT_SKIP_HOSTS apply to the adapters too.

API

  • ahn.check(host_or_url, error_class=None, failure_stage=None, *, timeout=4.0, adapter=None) -> Result | None — asks about the host of a hostname or URL. adapter is for integrations: a short lowercase name (such as langchain) that is added to the User-Agent and changes nothing else. Returns None, without raising, when there is no answer: network error, timeout, unreadable response, or a host that is never looked up (see below).
  • ahn.classify(exc_or_text, host=None) -> (error_class | None, failure_stage | None) — maps an exception or a message to human_verification, login_auth (401, 407), service_error (408, 429, 5xx, timeout) or site_access (402, 403, 451, refused or unresolvable connection), and to before_target_response when no connection to the site was made, or response_observed when an HTTP status came back. It reads exception class names and messages as text, so Playwright's TimeoutError, httpx/requests/aiohttp errors and similar are recognised without importing those libraries. The rules are those of the Claude Code plugin. host, when given, is the hostname the error is about; tokens naming it are ignored, so login.example.com is not read as a login wall (from 0.3.0). From version 0.2.1 there is no default class: an error that is neither a block nor an outage (404, 410, 400, 422 and every other 4xx outside the list above, or a message that names nothing) gives (None, None), and the caller should not look it up, because a mistyped address is not a site that refuses agents.
  • Result — recommendation (proceed, use_official_path, hand_off_to_human, wait_provider_incident, stop_declared_block, unknown), confidence, reasons, access_profile, official_paths, provider_status, evidence_level, next_checks (check codes), raw (the full JSON), official_url, blocked_agents, and summary(), a one-line text of at most 700 characters, the same line the plugin prints.
  • ahn.clear_cache() — forget cached answers.

Read the answer in the order of the API contract. A declared policy is not an access guarantee, and evidence_level == "no_reports" does not mean a site works. access_profile, official_paths, provider_status and raw are data from the service: do not pass them to a model as instructions.

What it sends

At most one request per host, error class and stage every 10 minutes:

GET https://agenthelpnetwork.com/v1/incidents/check?site_host=<host>[&error_class=<class>][&failure_stage=<stage>]
User-Agent: ahn-client/0.3.0 (+https://agenthelpnetwork.com)

The adapters send User-Agent: ahn-client/0.3.0 (+langchain; +https://agenthelpnetwork.com), with langchain, crewai, openai-agents, claude-agent-sdk or browser-use in place of langchain. That is the whole difference, and it lets the service count client kinds. The part before the first space is always ahn-client/<version>.

  • site_host is the bare lowercase hostname: scheme, credentials, port, path, query and fragment are dropped, and one leading www. is removed. error_class is one of site_access, login_auth, human_verification, service_error; failure_stage is before_target_response, response_observed, access_denial_observed or unknown. Any other value makes check return None without a request.
  • Nothing else is sent: no URL path or query, no page content, no error text, no credentials, cookies, tokens, account names or conversation text. There is no POST and no report.
  • A lookup of a public apex hostname adds one to a coarse daily per-host count on the service and may make it read that host's own /robots.txt and /llms.txt (at most once a day per host). See the policy.

What it never looks up

check returns None and sends nothing for: localhost, IP addresses (including short and hex forms such as 127.1), single-label names, names under .local, .internal, .test, .lan, .arpa, .onion, .invalid, .example, .home, .corp, .intranet, .private, .localdomain, .alt, .svc, .consul, .cluster, names that stand for an address (nip.io, sslip.io, xip.io, and any name with an IPv4 address written into it such as 192-168-1-5.example.com), a URL containing a backslash (a browser and a parser read it differently), names that IDNA 2003 and IDNA 2008 map to different domains (faß.de), and agenthelpnetwork.com and its subdomains. Nothing is resolved to decide this. Hostnames under a public suffix can still be internal (jenkins.yourcompany.com); list them in AHN_CLIENT_SKIP_HOSTS.

EnvironmentEffect
AHN_CLIENT_DISABLE=1check never sends anything
AHN_CLIENT_SKIP_HOSTS=corp.example.com,other.example.orgNever look up these hosts or their subdomains

Answers are cached in memory for 10 minutes (a failed lookup for 1 minute); nothing is written to disk. From version 0.2.0, an answer whose declared policy is still being read (access_profile.freshness is pending, next check profile_pending) is kept only for its retry_after_seconds (Result.retry_after_seconds), so the one repeat lookup the service asks for reaches it, and the summary line says when to look once more. Redirects from the service are not followed.

Development

From the repository root: python -m pytest tests/test_ahn_client.py tests/test_ahn_client_adapters.py -q (no network, no framework needed). The tests that run a real framework are skipped unless it is installed; install it in a separate virtual environment to run them. Build with python -m build clients/python.

License

Apache-2.0. Source: agent-help-network/agent-help-network, clients/python/.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.3.021 Oct 20264

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
agent-help-network/agent-help-network

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 21 Oct, 09:39 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:59ae00726f60355b507de8bd8c1c65a65691c4122871b2ae93bc97222478f152