Skip to content

Greenfield - designing a new agent with HumanChain

You’re building an AI agent from scratch and you want HumanChain to be a peer to the LLM from day one - not an afterthought you bolt on later when you hit your first lawsuit.

This page is the design template.

The mental model: skill graph, not chain-of-thought

The right framing isn’t “LLM with a HumanChain escape hatch.” It’s a skill graph where the agent picks a skill node based on the question’s domain and the action’s stakes. Some nodes are LLM-only; some are HumanChain-only; some are both.

┌──────────────────┐
│ user question │
└────────┬─────────┘
┌──────────────────┐
│ skill classifier │
└────────┬─────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ LLM │ │ Human- │ │ Hybrid │
│ alone │ │ Chain │ │ (LLM │
│ │ │ alone │ │ drafts, │
│ │ │ │ │ human │
│ │ │ │ │ signs │
│ │ │ │ │ off) │
└─────────┘ └──────────┘ └──────────┘

The classifier’s job is to pick which path each question takes. You build that classifier once; everything downstream gets simpler.

The trigger-then-act loop

Every action your agent takes follows this five-step shape:

  1. Receive - user question or upstream trigger
  2. Classify - is this LLM-only, HumanChain-only, or hybrid?
  3. Consult - if HumanChain is involved, call consult()
  4. Act - perform the action, gated on the response’s provenance
  5. Audit - log the decision trail (question, route, response, action) for downstream review

The audit step is the part most teams skip on day one and regret on day 90. Build it in from the start; it’s cheap when the schema is fresh.

@dataclass
class AgentDecision:
decision_id: str
user_question: str
classified_route: Literal["llm_only", "humanchain_only", "hybrid"]
consult_id: str | None # populated if humanchain was called
provenance: str | None
action_taken: str
action_kind: Literal["reversible", "costly", "irreversible"]
timestamp: int
def trigger_then_act(question: str) -> AgentDecision:
route = classify(question)
consult_response = None
if route in ("humanchain_only", "hybrid"):
consult_response = consult(question=question, ...)
action = execute(route, question, consult_response)
decision = AgentDecision(
decision_id=new_uuid(),
user_question=question,
classified_route=route,
consult_id=consult_response.consult_id if consult_response else None,
provenance=consult_response.provenance if consult_response else None,
action_taken=action.summary,
action_kind=action.reversibility,
timestamp=now_ms(),
)
audit_log.append(decision)
return decision

That’s the entire backbone. Every other concern is a refinement.

The four roles consult() plays

In a well-designed agent, consult() shows up in four distinct roles. Designing for all four from day one keeps the codebase clean:

Role 1: Oracle

The agent doesn’t know something. It asks. The human’s answer is treated as ground truth and applied directly.

# Question the agent can't safely answer on its own
answer = consult(
question="What's our standard counter-position on derivative IP?",
domain_tags=["legal", "contracts"],
target_responses=1,
approval_method="fastest",
)
apply_legal_position(answer.response_text)

Role 2: Auditor

The agent has a draft answer from its LLM. It asks a human to review. The human’s response either approves the draft or replaces it.

draft = llm.generate(user_question)
review = consult(
question=(
f"User asked: {user_question}\n\n"
f"Our draft answer:\n{draft}\n\n"
f"Approve, or rewrite?"
),
domain_tags=infer_tags(user_question),
target_responses=1,
approval_method="fastest",
)
if review.response_text.strip().lower().startswith("approve"):
return draft
else:
return review.response_text # human rewrote it

Role 3: Gatekeeper

The agent is about to do something irreversible. It asks for explicit approval before proceeding. Failure to get unanimous approval blocks the action.

approved = consult(
question=f"About to execute: {action.describe()}. Approve?",
context_brief=action.full_payload(),
domain_tags=action.domain_tags,
target_responses=3,
approval_method="all_must_agree",
wait_seconds=600,
require_human=True,
)
if approved.provenance != "human" or not is_approval(approved.response_text):
raise ActionGatedError(f"Action {action.id} did not pass human gate")

Role 4: Trainer

The agent surfaces answers it suspects are wrong (or low-confidence) for human correction. These don’t gate any user-facing action - they go into a feedback loop that retrains the agent’s own behaviour over time.

if draft.confidence < 0.4:
# Async background fire-and-forget
label = consult(
question=user_question,
context_brief=f"LLM draft (low conf):\n{draft.text}",
domain_tags=draft.tags,
wait_seconds=1800, # batch - no rush
)
training_set.add(question=user_question, label=label.response_text)

A greenfield agent should have all four roles wired by month three. Role 1 and Role 3 are the most user-visible; Role 2 and Role 4 are where quality compounds over time.

Designing for the LLM fallback

bypass_on_timeout=True is the default for a reason: your agent should never hard-stop because no human happened to be online. But a fallback is still a degraded response, and your codebase should treat it that way.

The pattern: branch on provenance once, downstream code stays clean.

def consult_with_quality_tiers(question: str) -> tuple[str, str]:
"""Returns (response_text, quality_tier) where tier is
'expert' | 'llm' | 'unavailable'."""
answer = consult(
question=question,
target_responses=1,
approval_method="fastest",
wait_seconds=90,
bypass_on_timeout=True,
)
if answer.provenance == "human":
return answer.response_text, "expert"
elif answer.provenance == "llm_fallback":
return answer.response_text, "llm"
else:
return "Could not get a response.", "unavailable"

Downstream UI treats expert and llm differently - maybe a small “answered by [name]” footer vs. “answered by HumanChain LLM fallback” disclosure. Users like knowing which tier they got; it builds trust in the system.

Onboarding your humans early

If you’re building an enterprise-scoped agent (humans on your team answer questions), get them onboarded to HumanChain before you ship the agent. Two reasons:

  1. Avoid the cold-start problem. An agent that always falls back to the LLM in the first week looks broken even if it’s working exactly as designed. Pre-seed your chain with 3–5 experts per domain so day-one calls have a real human to route to.
  2. Calibrate routing. expertise grading takes a few dozen answers per person to stabilise. Don’t wait for users to hit the agent before your experts answer questions; have them onboard with a few seed questions to bootstrap their scores.

The Nominated chains pattern page walks through the onboarding sequence in detail.

Anti-patterns to avoid

A few shapes that look clever but cost you later:

Anti-pattern: HumanChain as a try/except wrapper.

# Don't do this
try:
answer = llm.generate(question)
except:
answer = consult(question=question)

consult() isn’t a fallback for LLM failures - it’s a peer routing target for questions the LLM shouldn’t answer. Pick the route up front based on the question, not based on whether the LLM errored.

Anti-pattern: Bypassing the classifier “just for now.”

# Don't do this
def respond(question):
return llm.generate(question) # TODO: add humanchain when we have time

Adding HumanChain after the fact is exactly the retrofit problem (retrofit pattern). If you’re greenfield, wire the classifier in even if it always picks “LLM only” for now. The hook is the value.

Anti-pattern: Always escalating to humans “to be safe.”

# Don't do this
def respond(question):
return consult(question=question) # always ask a human

That’s not safe, it’s expensive and slow. Humans get burned out answering trivial questions; expertise grading can’t calibrate; users hate the latency. Pick your triggers (trigger patterns) and trust the LLM in the rest.

Next step

Most of the design choices above also matter for retrofitting. If you’re not greenfield, the retrofit pattern covers what changes:

Retrofit pattern →