Skip to content

x-protolabs/hitl-mode-v1 lets agents declare the approval policy for each skill on their agent card. HITL is a gradient, not a binary — this extension lets the dispatcher + HITL plugin route each skill invocation through the right flow without goal-level config.

Extension URI: https://proto-labs.ai/a2a/ext/hitl-mode-v1


The five modes ​

Ordered from least-gated to most-gated:

ModeSemantics
autonomousNo human in the loop. Task runs, outcome is what it is.
notificationRuns autonomously. A read-only notification is rendered on the originating surface (Discord, Linear) for awareness.
vetoShort TTL window after dispatch where a human can cancel via tasks/cancel before side effects complete. Auto-approved on TTL expiry.
gatedBlocking input-required before any side effect. No auto-approve; execution halts until a decision.
compoundMulti-checkpoint gated. The agent emits multiple input-required states across the task lifecycle (draft → review → publish); each requires its own decision.

HITL_MODE_ORDER ranks them autonomous=0 through compound=4 for numeric comparisons.


Reviewer resolution — sub-agent → caller ​

This is the part agent-authors most often get wrong. HITL doesn't always mean "human in the loop" — it can mean "dispatching agent in the loop."

Picture the chain: Ava dispatches pr_review to Quinn. Quinn needs the user's intent clarified on an ambiguous change. The input-required state should route back to Ava (the dispatcher), not straight to the human operator. Ava then chooses:

  1. Answer autonomously from context she already has, or
  2. Bubble up to the human if she genuinely needs their input

The reviewer precedence is:

input-required
  → dispatching agent (if present; agent-scoped resolution)
  → renderer chain (Discord, Linear, etc.) as final fallback

TaskTracker reads the original dispatch's source.agentId to decide the first hop. Sub-agent chains (Ava → Quinn → protoPen) walk the chain back until a resolver wants the question or the chain terminates at the operator.

A declaration can override this via reviewer:

json
{ "mode": "gated", "reviewer": "operator" }

Forces the prompt straight to the human for skills where the dispatching agent's judgment isn't sufficient (e.g. production deploys). Default without this field is the caller-first chain.


What the extension does ​

A read-side declaration — hitl-mode-v1 does NOT mutate outbound traffic or collect observations. It's policy metadata.

  • before(ctx) — stamps x-hitl-mode: <mode> (and x-hitl-veto-ttl-ms: <N> if set) on outbound metadata. Downstream consumers read these to decide how to render the prompt.
  • after(ctx) — no-op. Mode is policy, not observation.

Registration at startup in src/index.ts:

ts
import { registerHitlModeExtension } from "./executor/extensions/hitl-mode.ts";
registerHitlModeExtension();

Declaring it on your agent card ​

json
{
  "capabilities": {
    "extensions": [
      {
        "uri": "https://proto-labs.ai/a2a/ext/hitl-mode-v1",
        "params": {
          "skills": {
            "sitrep":              { "mode": "autonomous" },
            "open_pr":             { "mode": "notification" },
            "merge_pr":            { "mode": "veto", "vetoTtlMs": 300000 },
            "rebase_pr":           { "mode": "gated" },
            "production_deploy":   { "mode": "gated", "reviewer": "operator" },
            "publish_post":        { "mode": "compound" }
          }
        }
      }
    ]
  }
}

Keys:

  • mode (required) — one of the five values above
  • vetoTtlMs (optional) — veto-window length in ms, only meaningful for veto mode. Default: 60_000 (1 minute)
  • reviewer (optional) — "operator" forces the HITL prompt to the human; absent means caller-first chain (the default)
  • note (optional) — human-readable reason, shown in the HITL prompt

SkillBrokerPlugin parses the declarations into defaultHitlModeRegistry on every card refresh (every 10 min).


Consumers ​

The extension is declared on agent cards and parsed into defaultHitlModeRegistry at startup, but this build does not ship an in-process consumer for x-hitl-mode. The HITL renderer plugin was removed; if you need an approval gate, subscribe to the relevant prompt topic from your own plugin or external service and read the registry to decide which mode to apply.

  • Dashboard — fleet view shows which skills are declared gated vs. autonomous, useful for auditing agent declarations even when no approval flow is wired.

Registry API ​

ts
import {
  defaultHitlModeRegistry,
  HITL_MODE_ORDER,
  type HitlMode,
} from "../executor/extensions/hitl-mode.ts";

const decl = defaultHitlModeRegistry.get("quinn", "merge_pr");
// → { agentName: "quinn", skill: "merge_pr", mode: "veto", vetoTtlMs: 300000 }

if (decl && HITL_MODE_ORDER[decl.mode] >= HITL_MODE_ORDER.gated) {
  // Halt execution until human decides
}

Status ​

  • ✅ before hook ships on every A2A call; metadata stamping live
  • ✅ Registry populated by SkillBrokerPlugin card-refresh path
  • ✅ Caller-first reviewer resolution live — TaskTracker routes input-required back to the dispatching agent (via meta.dispatcherAgent on the originating agent.skill.request) before falling back to the human renderer chain. reviewer: "operator" on the declaration forces direct-to-human.

  • blast-v1 — per-skill scope declaration; compose with hitl-mode to gate high-impact skills
  • Build an A2A Agent — agent-author recipe for the A2A spec surface

protoWorkstacean — a switchboard, not an agent.