Async consults (start + poll)
The plain consult() call blocks until the Hub returns, which is the
right shape for most agents. But some questions take minutes because a
human has to read and think, and you may not want to hold a tool call
open that long. For those, humanchain-mcp 0.17.0 adds a fire-and-poll
pair.
The pattern
consult_start(...)submits the question and returns immediately with aconsult_job_id.consult_status(consult_job_id)returns the current state. Poll it on your own cadence until the status is terminal.
consult_start
Takes the same inputs as consult. Returns right away:
{ "consult_job_id": "…uuid…", "status": "QUEUED", "submitted_at": "2026-05-29T18:00:00Z", "poll_url": "/api/agent/consult/…/status", "cancel_url": "/api/agent/consult/…/cancel"}consult_status
{ "consult_job_id": "…uuid…", "status": "ANSWERED", "submitted_at": "…", "updated_at": "…", "completed_at": "…", # set once terminal "response": { ... ConsultResponse ... }, # present iff status == ANSWERED "error": null # {code, message} iff status == FAILED}Statuses: QUEUED, IN_FLIGHT, ANSWERED, TIMED_OUT, CANCELLED,
FAILED, and CLARIFY_PENDING (the human asked your agent for more
context before answering).
The terminal states — stop polling once you see one of these:
ANSWERED—responsecarries theConsultResponse. Checkresponse.provenanceto tell a human answer from an LLM fallback.TIMED_OUT— no human answered withinwait_secondsand you usedbypass_on_timeout=false(orrequire_human=true), so there is no answer. A human can still answer after the deadline: if one does, the status read carrieslate_answer: truepluslate_answer_text— the consult stays honestlyTIMED_OUT, but the late human input is not lost.FAILED— an error occurred;errorcarries{code, message}.CANCELLED— you (or a worker) cancelled the job.
QUEUED, IN_FLIGHT, and CLARIFY_PENDING are non-terminal — keep polling,
backing off toward ~30s to be a polite client.
The easy path: the Python SDK
In Python, don’t hand-roll the polling — the humanchain-sdk package hides it.
HubClient.consult() fires the consult and waits for the terminal result (it uses
the async API under the hood, so it isn’t bound by your HTTP layer’s
max-request-duration):
from humanchain import HubClient
async with HubClient.from_env() as client: # reads HC_API_KEY / HC_BACKEND_URL answer = await client.consult( "Is this SOW's IP-assignment clause standard for B2B SaaS?", domain_tags=["legal", "contracts"], wait_seconds=600, ) print(answer.provenance, answer.response_text)consult() returns the ConsultResponse directly and raises a typed
HumanChainError on FAILED/TIMED_OUT. Use submit_and_continue() to hold the
job handle and poll on your own cadence instead. Either way you never write the
polling loop yourself.
For non-Python stacks, the raw fire-and-poll loop is below.
A fire-and-poll loop (HTTP)
import os, time, httpx
B = os.environ.get("HUMANCHAIN_BACKEND", "https://hub.example.com")H = {"Authorization": f"Bearer {os.environ['HUMANCHAIN_API_KEY']}"}
job = httpx.post(f"{B}/api/agent/consult/start", headers=H, json={ "question": "Is this SOW's IP-assignment clause standard for B2B SaaS?", "domain_tags": ["legal", "contracts"], "wait_seconds": 600,}).json()
jid = job["consult_job_id"]while True: s = httpx.get(f"{B}/api/agent/consult/{jid}/status", headers=H).json() if s["status"] in ("ANSWERED", "TIMED_OUT", "CANCELLED", "FAILED"): break time.sleep(5)
if s["status"] == "ANSWERED": print(s["response"]["response_text"])else: print("no answer:", s["status"], s.get("error"))Cancel and clarify
POST /api/agent/consult/{id}/cancelstops a job you no longer need.- If
status == CLARIFY_PENDING, the human asked a follow-up. Answer it withPOST /api/agent/consult/{id}/clarifyand a body of{"answer": "..."}, and the consult resumes.
Next step
For the synchronous call and the full response shape, see the consult API reference.