Documentation / guides
Provide requested user input
Answer a model-initiated request for user input.
AskUser is a loop-owned gate. The model or tool supplies a question, Harness
publishes event.UserInputRequested, and the loop parks until a response is
delivered. The response is routed by the owning LoopID and the exact
ToolExecutionID; it is not sent to whichever loop is currently active.
Command and public contract
type ProvideUserInput struct {
Header
GateRoute
Answer string `json:"answer,omitempty"`
}
func (c ProvideUserInput) GateToolExecutionID() uuid.UUID
type Session interface {
RespondGate(context.Context, gate.GateResponse) error
}
The public caller supplies a gate.GateResponse; the runtime translates it into
the command above after validating the gate kind and response shape. There is no
command-level Ack. The waiting tool receives the answer after the durable gate
resolution succeeds.
func answerQuestion(ctx context.Context, s session.Session, gateID gate.ID, answer string) error {
values, err := json.Marshal(answer)
if err != nil {
return err
}
return s.RespondGate(ctx, gate.GateResponse{
GateID: gateID,
Action: "answer", // the action declared by the AskUser gate's controls
Values: map[string]json.RawMessage{"answer": values},
Source: gate.ResponseSource{Kind: gate.ResponseFromUser},
})
}
For a plain AskUser gate, the integration should use the action and value shape
declared by that gate’s payload. The exact action constant for a particular gate
is part of the gate package contract, not a free-form string invented by the
caller. The low-level command’s Answer field is the normalized value used by
the loop-owned ask-user route. If the host already receives an answer object
from a gate-specific adapter, pass its validated GateResponse directly.
Request and answer lifecycle
UserInputRequested contains the question and optional choices for rendering:
type UserInputRequested struct {
enduring
loopScoped
Header
ToolExecutionID uuid.UUID `json:"tool_execution_id,omitzero"`
Question string `json:"question,omitempty"`
Choices []string `json:"choices,omitempty"`
}
The event identifies the tool execution, while the gate ID is the answer route
held by the session. Keep both IDs in the UI model. Question and Choices are
rendering data, not proof that the gate is still open.
%%{init: {"theme":"dark"}}%%
sequenceDiagram
participant T as Tool call
participant L as Loop actor
participant S as Session
participant U as User interface
T->>L: open AskUser gate
L->>S: UserInputRequested{ToolExecutionID}
S-->>U: render question and choices
U->>S: RespondGate(GateID, values)
S->>S: validate gate is open and action is allowed
S->>S: append GateResolved
S->>L: ProvideUserInput{GateRoute, Answer}
L->>T: deliver answer and resume
RespondGate is exactly-once at the durable boundary. It claims the gate,
appends event.GateResolved, removes the answer slot, and dispatches after the
append. If the append fails, the claim is reverted and the user may retry. If
another client won the race, *session.GateError has GateNotFound or
GateNotReady.
if err := s.RespondGate(ctx, response); err != nil {
var gateErr *session.GateError
if errors.As(err, &gateErr) && gateErr.Kind == session.GateActionInvalid {
// The response does not belong to this gate kind. Do not resend it
// with a guessed action.
}
return err
}
The public answer is live form data. The durable record keeps the typed gate
resolution and redaction-aware audit, not an arbitrary host-owned answer map.
For host-owned form gates, use the separate session.GateHost contract and its
AwaitGateAnswer path; ProvideUserInput is specifically for the loop-owned
AskUser route.
The source of the command is pkg/command/provide_user_input.go
and the event is pkg/event/tool.go.
The gate behavior is proved by internal/sessionruntime/gates_test.go,
which is a runtime proof of route matching and invalid-action rejection.