Skip to content

Decouple needs_human state transitions from HITL side effects #628

Description

@mrcfps

Problem

needs_human is a normal, resumable agent outcome: the agent has reached a decision that requires human authority. Today its state transition is coupled to hitl.enabled.

For example, the fixer only accepts needs_human as HITL control flow when hitl.enabled is true. With HITL disabled, the same structured outcome falls back to manual_intervention and the loop is paused as a failure-like condition. This conflates two different semantics:

  • expected control flow: the agent intentionally requests a human decision;
  • operational failure: Looper, a tool, or an agent contract cannot continue correctly.

It also makes transport configuration an authority over the meaning of the agent's structured result. hitl.enabled currently controls more than delivery: prompts, state transitions, notifications, GitHub/Feishu transport activity, and some git lifecycle behavior are tied together.

Authority

A valid structured needs_human result from the active agent execution is the authority for entering awaiting_human. Transport configuration is not authority for reclassifying that result as a failure.

Desired behavior

State semantics and automated external side effects should be independent:

Agent outcome hitl.enabled Required result
valid needs_human false Persist the ask and atomically park the loop as awaiting_human; perform no automated external HITL side effects
valid needs_human true Persist and park as awaiting_human, then run the configured notification/answer transport
Looper/tool/protocol failure either Follow the normal failure or operational-hold path; never masquerade as awaiting_human

When hitl.enabled is false, “no automated external HITL side effects” means:

  • no GitHub WIP push or draft PR created for an ask;
  • no GitHub ask comment or awaiting-human label;
  • no Feishu decision card or inbox polling;
  • no automated HITL notification/outreach;
  • no automatic answer polling or transport-driven requeue.

The required local/durable state changes still occur: persist the question and resume metadata, cancel the active queue item, interrupt the current run, clear next_run_at, and expose awaiting_human through the control plane.

An explicit operator response through POST /api/v1/loops/{seq}/respond should remain available regardless of hitl.enabled. It should store the answer, requeue the loop, and resume the prior agent session when compatible.

Scope

  • Apply the same state contract to the existing fixer and worker human-ask paths.
  • Make the agent contract capable of producing a valid human ask independently of transport enablement.
  • Keep outbound notification and answer transports gated by hitl.enabled.
  • Preserve the existing awaiting_human ask/answer metadata and resume flow.
  • Document that a disabled transport can leave a loop silently parked until an operator discovers it through the control plane and responds explicitly.

Non-goals

  • Do not add another loop or queue status.
  • Do not redesign every existing use of manual_intervention in this issue.
  • Do not add a new transport, inference layer, gate, or persisted authority field.
  • Do not globally freeze every other loop associated with the same PR.
  • Do not change the policy for deciding whether reviewer feedback is in scope; this issue only defines what happens after an agent returns needs_human.

Acceptance criteria

  • With hitl.enabled=false, a valid fixer needs_human result ends in loop status awaiting_human, not queue status/failure kind manual_intervention.
  • With hitl.enabled=false, a valid worker ask sentinel follows the same awaiting_human contract.
  • Disabled-mode tests prove zero GitHub, Feishu, notification, and transport-poll side effects for the park.
  • /respond can answer and resume an awaiting_human loop while automated HITL side effects are disabled.
  • The answer is injected into the compatible prior native session and marked consumed only after a successful resumed turn.
  • With hitl.enabled=true, existing configured transport delivery and polling behavior continues to work.
  • Looper/tool/agent-protocol failures continue to use failure or operational-hold semantics and never enter awaiting_human merely because an operator may need to inspect them.
  • Contract/integration coverage verifies park → no automatic requeue → explicit answer → resume for both fixer and worker.

Trade-offs

A loop can become silently parked when transport side effects are disabled. That is intentional: awaiting_human remains visible in durable state and the control plane, while deployments that want proactive delivery can enable HITL transports. This is preferable to silently changing a valid human-decision request into an unrelated failure classification.

The existing hitl.enabled name becomes broader than its remaining responsibility. Keep it for compatibility in this change and document it as the gate for automated HITL outreach/transports rather than adding another configuration concept.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions