Skip to content

RFC: decouple HumanInputNode from host-specific HITL runtime via callback boundary #173

@QuantumGhost

Description

@QuantumGhost

AI disclosure: This issue was drafted and analyzed with Codex using GPT-5.4. I have reviewed the analysis, and I am responsible for the content.

Summary

This issue tracks the graphon side of a Human-in-the-Loop (HITL) boundary cleanup.

The goal is to make graphon own only the Human Input control-flow state machine, while the host application owns form schemas, session persistence, delivery, submission restoration, and other product-facing HITL semantics.

This issue is intentionally limited to graphon. Host-side changes will be handled separately.

Problem

Today, HumanInputNode still depends on host-facing runtime and form abstractions, and PauseReason still carries host-oriented payload.

That has a few practical costs:

  • graphon needs to know about form creation/loading semantics.
  • graphon pause payloads include form content, input schema, action schema, and resolved defaults.
  • graphon restores submitted payloads and owns behavior that should live in the host.
  • HITL changes tend to require synchronized edits in both graphon and the embedding application.

This makes the boundary too large and causes host product semantics to leak back into graphon.

Proposed boundary

The core protocol should be a decision callback:

from collections.abc import Callable, Mapping
from dataclasses import dataclass

from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
from graphon.variables.segments import Segment


@dataclass(frozen=True)
class HITLContext:
    workflow_execution_id: str
    node_id: str
    node_title: str
    variable_pool: ReadOnlyVariablePool


@dataclass(frozen=True)
class PauseRequested:
    session_id: str


@dataclass(frozen=True)
class Completed:
    selected_handle: str
    inputs: Mapping[str, Segment]
    outputs: Mapping[str, Segment]


@dataclass(frozen=True)
class Expired:
    selected_handle: str
    outputs: Mapping[str, Segment]


HITLDecision = PauseRequested | Completed | Expired
HITLCallback = Callable[[HITLContext], HITLDecision]

The pause reason should also be reduced to a minimal host lookup key:

from enum import StrEnum, auto
from typing import Literal

from pydantic import BaseModel


class PauseReasonType(StrEnum):
    HITL_REQUIRED = auto()


class HitlRequired(BaseModel):
    TYPE: Literal[PauseReasonType.HITL_REQUIRED] = PauseReasonType.HITL_REQUIRED
    session_id: str
    node_id: str
    node_title: str

HumanInputNode should become a pure control-flow translator:

  • build HITLContext
  • call HITLCallback
  • translate PauseRequested into PauseRequestedEvent
  • translate Completed / Expired into StreamCompletedEvent

Scope

  • add a callback-based HITL protocol to graphon
  • refactor HumanInputNode so it depends only on HITLCallback
  • minimize PauseReason for HITL to session_id, node_id, and node_title
  • remove host-specific Human Input runtime/form binding protocols from graphon
  • keep host-specific form, delivery, and submission semantics out of graphon

Non-goals

This issue does not cover host-side work such as:

  • HITL session storage
  • form snapshots
  • token / recipient / delivery behavior
  • API or history projection
  • replay enrichment

Acceptance criteria

  • HumanInputNode depends only on the new callback boundary
  • HITL pause events no longer expose form schema or delivery-oriented payload
  • completed and expired resume paths are driven by selected_handle plus runtime Segment mappings
  • graphon no longer exposes Human Input runtime/form repository binding as its public protocol
  • tests cover pause, completed, and expired callback paths

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions