askuser

package
v0.26.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package askuser holds the daemon-side coordinator for the ask_user MCP tool. The agent (claude / codex / gemini) calls `ask_user` over MCP; the handler registers a pending question + broadcasts SSE; the user answers in the web UI; the answer resolves the pending channel, and the MCP tool returns to the agent.

Lifecycle parallels gate.ApprovalManager but the trigger is an MCP RPC instead of a unix socket dial — there is no subprocess hook, just a blocking goroutine inside the wick process.

Index

Constants

View Source
const DefaultTimeout = 5 * time.Minute

DefaultTimeout is how long Ask blocks before returning an error to the calling agent. Five minutes is long enough for "the user stepped away to grab coffee" but short enough that a forgotten session doesn't pin a goroutine forever. Configurable per Ask via Question.Timeout.

Variables

This section is empty.

Functions

func SocketPath added in v0.17.0

func SocketPath(appName string) string

SocketPath returns the unix socket the server's ask listener binds and sibling processes dial. Lives next to agentctl.sock / the gate dir; security comes from the 0700 parent dir, like gate.sock.

Layout: ~/.<app>/agents/askuser.sock

Types

type Answer

type Answer struct {
	Value  string            `json:"value,omitempty"`
	Text   string            `json:"text,omitempty"`
	Values map[string]string `json:"values,omitempty"`
}

Answer is what the user posts via /sessions/{id}/answer. One of Value / Text / Values is set; if Value and Text are both present, Value wins (it's the label of a clicked option, more authoritative than free-typed text on the same form). Values carries the field map of a structured form ask.

type AskRequest

type AskRequest struct {
	ID            string   `json:"id"`
	SessionID     string   `json:"session_id"`
	AgentName     string   `json:"agent_name,omitempty"`
	Question      string   `json:"question"`
	Options       []Option `json:"options,omitempty"`
	AllowFreeform bool     `json:"allow_freeform,omitempty"`
	Fields        []Field  `json:"fields,omitempty"`
}

AskRequest is the broadcast payload — Question + a server-minted id used by both UI (in POST /answer) and the manager's pending map (to route the answer back).

type Asker added in v0.17.0

type Asker interface {
	Ask(q Question, done <-chan struct{}) (Answer, error)
}

Asker is the transport-agnostic ask contract. The in-process Manager implements it for the HTTP server; SocketAsker implements it for sibling processes (stdio MCP) by dialing the server's unix socket — same pattern as the gate binary dialing gate.sock, so an ask works from any local process without HTTP auth.

type Field added in v0.17.0

type Field struct {
	Key           string   `json:"key"`
	Label         string   `json:"label"`
	Type          string   `json:"type,omitempty"`
	Placeholder   string   `json:"placeholder,omitempty"`
	Value         string   `json:"value,omitempty"` // prefill; empty for secrets
	Required      bool     `json:"required,omitempty"`
	Help          string   `json:"help,omitempty"`
	Options       []Option `json:"options,omitempty"`        // choice/multi/dropdown
	AllowFreeform bool     `json:"allow_freeform,omitempty"` // add an "Other…" text box
}

Field is one question/input in a structured form ask. When Question.Fields is non-empty the UI renders a form modal instead of the plain option/freeform card, and the answer comes back as Answer.Values keyed by Field.Key.

Type drives the input widget:

  • "choice" — Options as single-select rows (pick one)
  • "multi" — Options as multi-select rows (pick many; the answer is a JSON-encoded array of the selected values)
  • "rank" — Options drag-reordered; the answer is a JSON-encoded array of values in the user's chosen order
  • "dropdown" — Options as a <select> (pick one, compact)
  • "text" — free text input (default when no Options)
  • "secret" — password input; plaintext handled server-side, never echoed to the agent
  • "number" — numeric input

In a multi-question ask (Question.Fields), the UI renders each field as one step in a wizard the user pages through and can skip.

AllowFreeform on a choice/dropdown field adds an "Other…" text box so the user can answer outside the preset Options.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns the per-process pending-asks map. Concurrent-safe; caller is expected to wire OnRequest/OnResolved into the SSE broadcaster so the UI sees state changes.

func NewManager

func NewManager(opt Options) *Manager

NewManager constructs an empty manager.

func (*Manager) Ask

func (m *Manager) Ask(q Question, done <-chan struct{}) (Answer, error)

Ask registers a pending question, fires onRequest, and blocks until the user answers, the timeout fires, or done is closed.

Returns Answer + error. error is non-nil only on timeout / cancel — agent-side handlers convert that into a tool error so the LLM can decide to retry or give up.

func (*Manager) PendingFor

func (m *Manager) PendingFor(sessionID string) []AskRequest

PendingFor returns a snapshot of in-flight asks for one session. Used by the UI for reconnect rehydrate.

func (*Manager) Resolve

func (m *Manager) Resolve(requestID string, ans Answer) bool

Resolve delivers an answer to the matching pending Ask. Returns false if the id is unknown — typical when the agent gave up or the request already timed out.

type Option

type Option struct {
	Label       string `json:"label"`
	Value       string `json:"value"`
	Description string `json:"description,omitempty"`
}

Option is one choice presented to the user. Label = what they see, Value = what gets returned to the agent, Description = optional secondary line shown under the label in the wizard.

type Options

type Options struct {
	DefaultTimeout time.Duration
	OnRequest      func(AskRequest)
	OnResolved     func(sessionID, requestID string)
}

Options wires callbacks. Both are optional; nil = no broadcast.

type Question

type Question struct {
	SessionID     string        `json:"session_id"`
	AgentName     string        `json:"agent_name,omitempty"`
	Question      string        `json:"question"`
	Options       []Option      `json:"options,omitempty"`
	AllowFreeform bool          `json:"allow_freeform,omitempty"`
	Fields        []Field       `json:"fields,omitempty"`
	Timeout       time.Duration `json:"-"`
}

Question is the input to Manager.Ask. Mirrors the MCP tool's input schema 1:1 so the handler can pass it through unchanged.

type SocketAsker added in v0.17.0

type SocketAsker struct {
	Path string
}

SocketAsker dials the server's askuser socket. Implements Asker so the MCP handlers can use it interchangeably with the in-process Manager. Construction is cheap and dial happens per Ask — the server may start or restart at any time relative to this process.

func (*SocketAsker) Ask added in v0.17.0

func (a *SocketAsker) Ask(q Question, done <-chan struct{}) (Answer, error)

Ask sends one Question over the socket and blocks until the answer comes back, the server-side timeout fires, or done closes. A dial failure means no wick server (or one too old to serve asks) is running on this machine.

type SocketServer added in v0.17.0

type SocketServer struct {
	// contains filtered or unexported fields
}

SocketServer accepts ask requests from sibling processes and resolves them through the wrapped Manager (pending map + SSE + web-UI answer — identical path to an in-process ask).

func ServeSocket added in v0.17.0

func ServeSocket(path string, mgr *Manager) (*SocketServer, error)

ServeSocket binds path and serves asks against mgr until Close. Stale socket files from a crashed previous run are removed first, mirroring gate.NewListener.

func (*SocketServer) Close added in v0.17.0

func (s *SocketServer) Close() error

Close stops the accept loop and removes the socket file. In-flight asks resolve through their own conn-scoped done channels.

func (*SocketServer) SocketPath added in v0.17.0

func (s *SocketServer) SocketPath() string

SocketPath returns the bound socket path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL