Documentation
¶
Overview ¶
Package-shared MCP form-elicitation core: translates an MCP elicitation requestedSchema into the strict ask_user question payload and maps the submitted answers back into schema-conformant content. Runtime adapters (ACP, codex) own only their protocol envelopes around this core.
Index ¶
- Constants
- Variables
- func ApplyInteractionOp(payload UIPayload, state TextInteractionState, op InteractionOp) (TextInteractionState, InteractionOutcome)
- func DeferredMetadata(req Request) map[string]any
- func ElicitationURLInput(message, address string) (any, error)
- func ResultBytes(req Request) []byte
- func ValidateAnswers(payload UIPayload, answers []QuestionAnswer) error
- func ValidateAskUserInput(input any) error
- type AdvanceInteractionInput
- type AdvanceInteractionResult
- type AdvanceTextInput
- type AdvanceTextResult
- type CancelInput
- type CreatePendingInput
- type ElicitationFormMapping
- type FlowRequest
- type FlowResult
- type FlowService
- type InteractionOp
- type InteractionOpKind
- type InteractionOutcome
- type InteractionReject
- type QuestionAnswer
- type Request
- type ResolveInput
- type Service
- func (s *Service) AdvanceInteraction(ctx context.Context, input AdvanceInteractionInput) (AdvanceInteractionResult, error)
- func (s *Service) AdvanceText(ctx context.Context, input AdvanceTextInput) (AdvanceTextResult, error)
- func (s *Service) CanRespond(req Request) bool
- func (s *Service) Cancel(ctx context.Context, input CancelInput) (Request, error)
- func (s *Service) CancelPendingForRun(ctx context.Context, botID, sessionID, runID string, fencingToken int64, ...) ([]Request, error)
- func (s *Service) CancelPendingForSession(ctx context.Context, botID, sessionID, reason string) ([]Request, error)
- func (s *Service) CreatePending(ctx context.Context, input CreatePendingInput) (Request, error)
- func (s *Service) Fail(ctx context.Context, requestID string, result map[string]any) (Request, error)
- func (s *Service) Get(ctx context.Context, requestID string) (Request, error)
- func (s *Service) HasWaiter(requestID string) bool
- func (s *Service) ListBySession(ctx context.Context, botID, sessionID string) ([]Request, error)
- func (s *Service) ListBySessionToolCalls(ctx context.Context, botID, sessionID string, toolCallIDs []string) ([]Request, error)
- func (s *Service) ListPendingBySession(ctx context.Context, botID, sessionID string) ([]Request, error)
- func (s *Service) RegisterWaiter(requestID string) func()
- func (s *Service) ResolveTarget(ctx context.Context, input ResolveInput) (Request, error)
- func (s *Service) Submit(ctx context.Context, input SubmitInput) (Request, error)
- func (s *Service) UpdateAssistantMessage(ctx context.Context, requestID, messageID string) (Request, error)
- func (s *Service) UpdatePromptMessage(ctx context.Context, requestID, promptMessageID, externalID string) (Request, error)
- func (s *Service) UpdateToolResultMessage(ctx context.Context, requestID, messageID string) (Request, error)
- func (s *Service) WaitForRegisteredResponse(ctx context.Context, requestID string) (Request, error)
- func (s *Service) WaitForResponse(ctx context.Context, requestID string) (Request, error)
- type SubmitInput
- type TextInteractionState
- type UIAnswer
- type UIOption
- type UIPayload
- type UIQuestion
Constants ¶
const ( ToolNameAskUser = "ask_user" // Provider sources record which surface created a request, for audit and // display only. Nothing classifies on them: whether an answer feeds an // in-process waiter or a native continuation is decided by the session's // runtime (runtimekind.UsesDecisionWaiter), the same as tool approvals. ProviderSourceACPMCP = "acp_mcp" ProviderSourceACPElicitation = "acp_elicitation" ProviderSourceCodexUserInput = "codex_request_user_input" ProviderSourceCodexElicitation = "codex_mcp_elicitation" StatusPending = "pending" StatusSubmitted = "submitted" StatusCanceled = "canceled" StatusExpired = "expired" StatusFailed = "failed" DeferredKind = "user_input" // PayloadVersion is the canonical ask_user payload version written to // storage. Older rows are upgraded on read by PayloadFromStored. PayloadVersion = 2 QuestionKindSingleSelect = "single_select" QuestionKindMultiSelect = "multi_select" QuestionKindText = "text" MaxQuestionsPerRequest = 4 MinOptionsPerQuestion = 2 MaxOptionsPerQuestion = 20 )
const DefaultWaitTimeout = 10 * time.Minute
DefaultWaitTimeout bounds how long ask_user waits for a response before canceling the request.
Variables ¶
var ( ErrNotFound = errors.New("user input request not found") ErrAlreadyDecided = errors.New("user input request already decided") ErrForbidden = errors.New("user input forbidden") )
var ErrInvalidAskUserInput = errors.New("invalid ask_user input")
Functions ¶
func ApplyInteractionOp ¶
func ApplyInteractionOp(payload UIPayload, state TextInteractionState, op InteractionOp) (TextInteractionState, InteractionOutcome)
ApplyInteractionOp is the pure transition function for button-driven input. It mirrors the plain-text transitions in advanceTextState where the semantics overlap (advance-on-answer, complete-on-last, skip fill).
func DeferredMetadata ¶
func ElicitationURLInput ¶ added in v0.20.0
ElicitationURLInput adapts a browser step to the shared user-input surface. Keep server wording and option identities intact until a surface renders them. Question text is plain on every surface, so the address stands on its own line: the web form links it, channels show it verbatim.
func ResultBytes ¶
func ValidateAnswers ¶
func ValidateAnswers(payload UIPayload, answers []QuestionAnswer) error
submittedResult validates the user's answers against the stored payload and builds the tool result returned to the model. Every question needs an explicit entry so a deliberate skip cannot be confused with a broken client payload.
func ValidateAskUserInput ¶
ValidateAskUserInput reports whether the arguments form a valid ask_user payload. CreatePending still parses again and owns the write-side normalization boundary.
Types ¶
type AdvanceInteractionInput ¶
type AdvanceInteractionInput struct {
BotID string
RequestID string
Op InteractionOp
}
type AdvanceInteractionResult ¶
type AdvanceInteractionResult struct {
// Handled is false when the request no longer exists or is not pending
// (already answered, canceled, expired) — callers should tell the user
// the interaction ended rather than surface an error.
Handled bool
// Changed reports whether state was persisted; unchanged ops (same-page
// nav, replay after completion) need no card re-render.
Changed bool
Reject InteractionReject
Request Request
}
type AdvanceTextInput ¶
type AdvanceTextResult ¶
type CancelInput ¶
type CreatePendingInput ¶
type CreatePendingInput struct {
BotID string
SessionID string
RouteID string
ChannelIdentityID string
WorkspaceTargetID string
RequestedByChannelIdentityID string
ToolCallID string
ToolName string
Input any
ProviderMetadata map[string]any
SourcePlatform string
ReplyTarget string
ConversationType string
ExpiresAt *time.Time
}
type ElicitationFormMapping ¶ added in v0.20.0
type ElicitationFormMapping struct {
// contains filtered or unexported fields
}
func ElicitationFormInput ¶ added in v0.20.0
type FlowRequest ¶
type FlowResult ¶
type FlowResult struct {
Request Request
}
func RunFlow ¶
func RunFlow(ctx context.Context, svc FlowService, flow FlowRequest) (FlowResult, error)
RunFlow executes the blocking ask_user state machine shared by native tool runtimes: create the pending request, publish it, wait for a response, and cancel on timeout, abort, or delivery failure.
type FlowService ¶
type FlowService interface {
CreatePending(ctx context.Context, input CreatePendingInput) (Request, error)
Cancel(ctx context.Context, input CancelInput) (Request, error)
WaitForRegisteredResponse(ctx context.Context, requestID string) (Request, error)
RegisterWaiter(requestID string) func()
}
FlowService is the subset of Service needed by the shared ask_user flow.
type InteractionOp ¶
type InteractionOp struct {
Kind InteractionOpKind
// QuestionIndex/OptionIndex address buttons for select/toggle; indexes
// are stable because UIPayload question/option order never changes after
// CreatePending.
QuestionIndex int
OptionIndex int
// Page is the navigate target.
Page int
// QuestionID binds set_text to the question that requested free text,
// not to the current cursor — navigation may have moved since the
// prompt was issued.
QuestionID string
Text string
}
type InteractionOpKind ¶
type InteractionOpKind string
Structured ask_user gestures from channels with native controls (inline buttons). They drive the same durable TextInteractionState as plain-text replies (AdvanceText) — one state machine, two input surfaces — so an interaction survives process restarts regardless of which surface it uses.
const ( // OpSelectOption answers a single_select question and auto-advances; // re-selecting the current choice clears it (toggle-off). OpSelectOption InteractionOpKind = "select" // OpToggleOption flips one multi_select option; never advances. OpToggleOption InteractionOpKind = "toggle" // OpSetText records a verbatim free-text answer (text question or // allow_custom). Unlike AdvanceText it never option-matches the input: // the user explicitly chose to type, so the text is taken as-is. OpSetText InteractionOpKind = "set_text" OpNavigate InteractionOpKind = "navigate" // OpSubmit completes the request, marking unanswered questions skipped. OpSubmit InteractionOpKind = "submit" )
type InteractionOutcome ¶
type InteractionOutcome struct {
Changed bool
Reject InteractionReject
}
type InteractionReject ¶
type InteractionReject string
InteractionReject explains why an op did not apply. Rejections are user-correctable (wrong button state, empty text) — not errors.
const ( RejectNone InteractionReject = "" RejectInvalidOp InteractionReject = "invalid_op" RejectCustomNotAllowed InteractionReject = "custom_not_allowed" RejectEmptyText InteractionReject = "empty_text" )
type QuestionAnswer ¶
type QuestionAnswer struct {
QuestionID string `json:"question_id"`
OptionIDs []string `json:"option_ids,omitempty"`
CustomText string `json:"custom_text,omitempty"`
Text string `json:"text,omitempty"`
Skipped bool `json:"skipped,omitempty"`
}
QuestionAnswer is the user's answer to a single question of an ask_user request. Selections are always arrays; single_select is an array of one.
type Request ¶
type Request struct {
ID string `json:"id"`
BotID string `json:"bot_id"`
SessionID string `json:"session_id"`
RouteID string `json:"route_id,omitempty"`
ChannelIdentityID string `json:"channel_identity_id,omitempty"`
WorkspaceTargetID string `json:"workspace_target_id,omitempty"`
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
ShortID int `json:"short_id"`
Status string `json:"status"`
Input map[string]any `json:"input,omitempty"`
UIPayload UIPayload `json:"ui_payload"`
Interaction TextInteractionState `json:"interaction"`
InteractionRevision int `json:"interaction_revision"`
Result map[string]any `json:"result,omitempty"`
ProviderMetadata map[string]any `json:"-"`
PromptExternalMessageID string `json:"prompt_external_message_id,omitempty"`
SourcePlatform string `json:"source_platform,omitempty"`
ReplyTarget string `json:"reply_target,omitempty"`
ConversationType string `json:"conversation_type,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
RespondedAt *time.Time `json:"responded_at,omitempty"`
CanceledAt *time.Time `json:"canceled_at,omitempty"`
RuntimeFenced bool `json:"-"`
}
type ResolveInput ¶
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func (*Service) AdvanceInteraction ¶
func (s *Service) AdvanceInteraction(ctx context.Context, input AdvanceInteractionInput) (AdvanceInteractionResult, error)
AdvanceInteraction applies one structured op to the durable interaction state under the same optimistic-lock CAS as AdvanceText: each attempt re-reads the row and re-applies the op, so a concurrent writer never makes a stale gesture win.
func (*Service) AdvanceText ¶
func (s *Service) AdvanceText(ctx context.Context, input AdvanceTextInput) (AdvanceTextResult, error)
AdvanceText consumes one plain-text reply without submitting the request. The final answer set is persisted first so a process crash before Submit can resume the same completion instead of losing earlier questions.
func (*Service) CanRespond ¶
CanRespond reports whether a waiter-backed request can accept a response in this process. Native chat requests are DB-deferred and must not use this helper; callers classify by the session's runtime (UsesDecisionWaiter), the same way tool approvals do.
func (*Service) CancelPendingForRun ¶ added in v0.20.0
func (s *Service) CancelPendingForRun(ctx context.Context, botID, sessionID, runID string, fencingToken int64, reason string) ([]Request, error)
CancelPendingForRun invalidates only pending ask_user requests owned by one exact run. It is used by runtime recovery after a run is declared lost; session-wide cancellation would incorrectly expire a newer run's request.
func (*Service) CancelPendingForSession ¶
func (*Service) CreatePending ¶
func (*Service) HasWaiter ¶
HasWaiter reports whether anyone in this process is currently registered for the request. It is only a local fast-path signal; DB status remains the cross-process source of truth for whether a request can accept a response.
func (*Service) ListBySession ¶
func (*Service) ListBySessionToolCalls ¶
func (*Service) ListPendingBySession ¶
func (*Service) RegisterWaiter ¶
RegisterWaiter records that a caller in this process owns the request's resolution. Callers that announce a pending request to users must register BEFORE announcing, or an instant response can be misjudged as orphaned. The returned release must run when the wait ends.
func (*Service) ResolveTarget ¶
func (*Service) UpdateAssistantMessage ¶
func (*Service) UpdatePromptMessage ¶
func (*Service) UpdateToolResultMessage ¶
func (*Service) WaitForRegisteredResponse ¶
WaitForRegisteredResponse waits like WaitForResponse but assumes the caller already registered with RegisterWaiter before announcing the request.
func (*Service) WaitForResponse ¶
WaitForResponse blocks until the request leaves pending. Resolution inside this process arrives via the Submit/Cancel/Fail broadcast; the slow ticker is only a safety net for transitions this process cannot observe (another node, manual DB changes, time-based expiry).
type SubmitInput ¶
type SubmitInput struct {
RequestID string
ActorChannelIdentityID string
Answers []QuestionAnswer
}
type TextInteractionState ¶
type TextInteractionState struct {
QuestionIndex int `json:"question_index"`
Answers []QuestionAnswer `json:"answers,omitempty"`
Completed bool `json:"completed,omitempty"`
}
TextInteractionState is the durable ask_user cursor shared by every input surface — plain-text replies (AdvanceText) and native buttons (AdvanceInteraction). Answers remain present when the user moves backward.
func (TextInteractionState) Answer ¶
func (s TextInteractionState) Answer(questionID string) (QuestionAnswer, bool)
Answer returns the saved answer for a question. ok is false when the user has not answered it yet (a persisted skip still counts as answered).
type UIAnswer ¶
type UIAnswer struct {
QuestionID string `json:"question_id"`
Question string `json:"question"`
Selected []UIOption `json:"selected,omitempty"`
CustomText string `json:"custom_text,omitempty"`
Text string `json:"text,omitempty"`
Skipped bool `json:"skipped,omitempty"`
} // @name userinput.UIAnswer
UIAnswer is the public, display-safe projection of one submitted answer. Model-only instructions remain in Request.Result and are never exposed here.
func AnswersFromResult ¶
AnswersFromResult returns only the display-safe answer fields from a persisted tool result. Internal instructions and failure diagnostics stay private to the runtime/model boundary.
func AnswersFromStored ¶
AnswersFromStored normalizes either in-memory or JSON-decoded answer data.
type UIOption ¶
type UIOption struct {
LabelKey string `json:"label_key,omitempty"`
ID string `json:"id"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
} // @name userinput.UIOption
UIOption describes one selectable option in an ask_user question.
type UIPayload ¶
type UIPayload struct {
Version int `json:"version"`
Questions []UIQuestion `json:"questions"`
}
UIPayload is the canonical, normalized ask_user payload (v2). It is the single shape stored, streamed, and rendered; ParseAskUserPayload is the only writer and PayloadFromStored the only reader.
func ParseAskUserPayload ¶
ParseAskUserPayload is the single write-side entry point for ask_user arguments. It validates strictly (no aliases, no inference) and returns the canonical v2 payload with server-generated question/option IDs.
func PayloadFromStored ¶
PayloadFromStored is the single read-side entry point. It decodes a stored or streamed ui_payload value, upgrading legacy (pre-v2) rows so the rest of the system only ever sees the canonical shape. It is tolerant: stored data must keep rendering even when it predates current validation.
type UIQuestion ¶
type UIQuestion struct {
ID string `json:"id"`
Text string `json:"text"`
Kind string `json:"kind"`
Options []UIOption `json:"options,omitempty"`
AllowCustom bool `json:"allow_custom,omitempty"`
CustomExclusive bool `json:"custom_exclusive,omitempty"`
// Required is tri-state for compatibility. Legacy/native ask_user payloads
// omit it and keep their existing surface semantics; ACP forms set it
// explicitly so false means an optional schema property.
Required *bool `json:"required,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
} // @name userinput.UIQuestion
UIQuestion describes one question in the ask_user UI payload.