askuserquestion

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: MIT Imports: 9 Imported by: 0

README

askuserquestion-go

Portable Go implementation of Claude Code's AskUserQuestion tool, packaged as a reusable charm.land/fantasy AgentTool.

The tool itself is host-rendered: the model emits a structured tool call with a list of multiple-choice questions, the host renders a picker, the user selects, and the canonical answer string flows back as the tool result. This module owns the schema, validation, and canonical answer formatting. Hosts supply a Resolver that actually talks to the user (TUI modal, web form, stdin prompt, mock, whatever).

Why split this out

The AskUserQuestion semantics — 1-4 questions, 2-4 options each, an implicit "Other" free-text option, a specific tool-result string format — are fixed by the Claude Agent SDK. They have nothing to do with which agent host you're running. Putting them in their own module means:

  • one canonical implementation, reusable across Crush, custom CLIs, web agents, headless workers
  • versionable schema (v0.1.0, etc.) without coupling to a host's internal package layout
  • testable in isolation — table-driven validation, golden answer formats, no UI dependencies

Install

go get github.com/dreamware-nz/askuserquestion-go

Requires Go 1.26+ (because of charm.land/fantasy).

Quick start

import (
    "github.com/dreamware-nz/askuserquestion-go"
)

// 1. Implement a Resolver — the seam that actually asks the user.
type myResolver struct{ /* TUI handle, channels, whatever */ }
func (r *myResolver) Ask(ctx context.Context, req askuserquestion.Request) ([]askuserquestion.Answer, error) {
    // push req onto your UI, block on the user's selection, return answers
    // in the same order as req.Questions.
}

// 2. Register the tool with your fantasy-based agent.
tool := askuserquestion.NewTool(&myResolver{})
agent.RegisterTool(tool)

There's a working CLI demo in examples/cli/ that wires the tool to stdin/stdout — useful for sanity-checking the schema and the canonical answer format without booting a real model.

go run ./examples/cli

Tool name

By default the tool registers as ask_user_question (snake case, to match common Go agent-host conventions). To match the official Claude Agent SDK spelling exactly:

tool := askuserquestion.NewToolNamed(askuserquestion.SDKToolName, resolver)
// → "AskUserQuestion"

Schema

Matches the Claude Agent SDK wire format exactly:

{
  "questions": [
    {
      "question": "Which auth method?",
      "header": "Auth",
      "multiSelect": false,
      "options": [
        { "label": "OAuth (Recommended)", "description": "Browser flow" },
        { "label": "API key",             "description": "Static token" }
      ]
    }
  ]
}

Hard limits enforced by Validate:

Field Rule
questions 1-4 entries
question non-empty
header non-empty (12-char cap is advisory)
options 2-4 entries
option.label non-empty, unique within the question

The 12-char header cap is advisory only. Validation does not reject long headers so a misbehaving model can't poison the conversation; renderers are expected to truncate.

Canonical answer format

Format(req, answers) produces the string Anthropic expects back as the tool result:

Auth method?
OAuth

Languages?
- Go
- Rust

Name?
Vincent Adultman

Rules:

  • questions joined by a blank line, in original order
  • single-select: bare label
  • multi-select: each label prefixed with -
  • Answer.Other (free text from the implicit "Other" option) overrides Answer.Selected and is emitted verbatim

Resolvers

The module ships three:

  • StaticResolver — canned answers, for tests and dry runs.
  • ResolverFunc — adapter for ad-hoc functions.
  • StdinResolver — numbered prompts on stdout, picks on stdin. Production hosts should write a richer UI; this exists for examples and smoke tests.

For real agent hosts (Crush, web servers, etc.) you'll usually want a Resolver that:

  1. Publishes the Request onto a broker / channel that the UI is listening on.
  2. Blocks (respecting ctx) until the UI sends back the user's selection.
  3. Returns ErrResolverCancelled if the user dismisses or supersedes the request (e.g. sends a new chat message instead of answering).

The tool surfaces ErrResolverCancelled as a non-error tool result with body [cancelled by user] so the conversation stays valid.

License

MIT. The tool description prose, schema, and canonical answer format follow Anthropic's public Claude Code / Claude Agent SDK design. This module is an independent reimplementation in Go.

Documentation

Overview

Package askuserquestion implements Claude Code's AskUserQuestion tool as a portable Go library that plugs into any charm.land/fantasy-based agent host.

The tool is host-rendered: the model emits a structured tool call, the host renders a picker UI, the user selects, and the canonical answer string is returned as the tool result. This package owns the schema, validation, and canonical formatting; hosts supply a Resolver that actually talks to the user (TUI modal, stdin prompt, web form, mock, whatever).

Index

Constants

View Source
const (
	MinOptions = 2
	MaxOptions = 4
)

MinOptions / MaxOptions bound the options-per-question range.

View Source
const MaxQuestions = 4

MaxQuestions is the upper bound on questions per call (Claude Agent SDK).

View Source
const RecommendedHeaderLen = 12

RecommendedHeaderLen is the soft cap on Header length from the SDK prose. It is advisory: Validate does not reject headers longer than this so a misbehaving model does not break the conversation, but renderers are expected to truncate to this width.

View Source
const SDKToolName = "AskUserQuestion"

SDKToolName is the spelling used by Anthropic's Claude Agent SDK.

View Source
const ToolName = "ask_user_question"

ToolName is the canonical tool name exposed to the model.

The original Claude Agent SDK calls it "AskUserQuestion". We expose snake case as the default to match Crush's naming conventions; hosts that want the SDK-compatible spelling can pass a custom name to NewToolNamed.

Variables

View Source
var (
	// ErrNoQuestions means the model called the tool with an empty questions
	// array. The SDK requires at least one question.
	ErrNoQuestions = errors.New("askuserquestion: questions must be a non-empty array")
	// ErrTooManyQuestions means more than 4 questions were supplied.
	ErrTooManyQuestions = errors.New("askuserquestion: maximum 4 questions allowed")
	// ErrEmptyQuestion means a Question.Question field was empty.
	ErrEmptyQuestion = errors.New("askuserquestion: each question must have non-empty question text")
	// ErrEmptyHeader means a Question.Header field was empty.
	ErrEmptyHeader = errors.New("askuserquestion: each question must have a non-empty header")
	// ErrOptionCount means options length was outside [2, 4].
	ErrOptionCount = errors.New("askuserquestion: each question must have 2-4 options")
	// ErrEmptyLabel means an Option.Label field was empty.
	ErrEmptyLabel = errors.New("askuserquestion: each option must have a non-empty label")
	// ErrDuplicateLabel means a single question listed the same label twice.
	ErrDuplicateLabel = errors.New("askuserquestion: option labels within a question must be unique")
)

Validation errors. Wrapped by Validate so callers can check with errors.Is.

View Source
var ErrResolverCancelled = errors.New("askuserquestion: resolver cancelled by user")

ErrResolverCancelled is returned by Resolvers when the user dismisses or supersedes the request (for example by sending a fresh chat message before answering). The tool surfaces this as a non-error tool result so the conversation stays valid.

Functions

func Description

func Description() string

Description returns the canonical tool description shipped with the SDK. Exposed so hosts that build their own ToolInfo can reuse the exact prose Claude was trained against.

func Format

func Format(req Request, answers []Answer) string

Format renders the user's answers into the canonical tool-result string used by the Claude Agent SDK. The format is:

<question 1>
- <label>
- <label>

<question 2>
<label>

Rules:

  • questions are joined by a blank line in the original Question order
  • if Answer.Other is set, its text is emitted verbatim as the body
  • if Question.MultiSelect is true, each selected label is prefixed "- "
  • if Question.MultiSelect is false, the single label is emitted bare
  • answers are paired by index; missing answers are skipped

Format is deterministic and side-effect-free; it is the only canonical answer formatter in this package.

func NewTool

func NewTool(r Resolver) fantasy.AgentTool

NewTool builds a fantasy.AgentTool using the default tool name (ToolName) and the given Resolver. The returned tool validates inputs against the Claude Agent SDK schema, hands valid requests to the Resolver, and formats the user's answers using Format.

On validation failure the tool returns a non-error ToolResponse with IsError=true so the model can self-correct on the next turn. On resolver cancellation (ErrResolverCancelled) it returns a non-error response with a stable "[cancelled]" body so the conversation stays valid.

func NewToolNamed

func NewToolNamed(name string, r Resolver) fantasy.AgentTool

NewToolNamed is like NewTool but lets the host override the tool name. Pass SDKToolName ("AskUserQuestion") for strict Claude Agent SDK parity.

func Validate

func Validate(p Params) error

Validate enforces the structural rules from the Claude Agent SDK schema:

  • 1 <= len(Questions) <= MaxQuestions
  • each Question has non-empty Question and Header
  • MinOptions <= len(Options) <= MaxOptions
  • each Option has a non-empty Label
  • Option labels are unique within a question

Validate returns one of the sentinel errors from errors.go so callers can branch on errors.Is. The header length cap is advisory and not checked.

Types

type Answer

type Answer struct {
	// Question is the original question text. Used to pair answers back to
	// questions when order is unstable.
	Question string
	// Selected is the list of option labels the user chose. For single-select
	// questions this has at most one element.
	Selected []string
	// Other is free-text the user typed when they picked the implicit "Other"
	// option. If set, Selected is ignored by Format.
	Other string
}

Answer is the user's response to a single Question.

Exactly one of Selected or Other should be populated. Hosts that allow the user to pick "Other" set Other to the typed text and leave Selected empty.

type Option

type Option struct {
	// Label is the short display text for this option.
	Label string `json:"label" description:"The display text for this option"`
	// Description explains what selecting this option means.
	Description string `json:"description" description:"Explanation of what this option means"`
}

Option is one choice within a Question.

type Params

type Params struct {
	// Questions is the ordered list of questions to ask. 1-4 entries.
	Questions []Question `json:"questions" description:"Questions to ask the user (1-4 questions)"`
}

Params is the top-level input schema for the tool.

JSON tags match the Claude Agent SDK wire format exactly so models trained against the SDK can drive this implementation without prompt adjustments.

type Question

type Question struct {
	// Question is the full prompt shown to the user.
	Question string `json:"question" description:"The complete question to ask the user"`
	// Header is a very short label (<= 12 chars) used as a column header or
	// compact title in the picker UI. Examples: "Auth method", "Library".
	Header string `json:"header" description:"Very short label (max 12 chars). Examples: \"Auth method\", \"Library\""`
	// Options are the available choices. 2-4 entries.
	Options []Option `json:"options" description:"The available choices (2-4 options)"`
	// MultiSelect allows the user to pick more than one option.
	MultiSelect bool `json:"multiSelect" description:"Set to true to allow multiple selections"`
}

Question is a single question with multiple-choice options.

type Request

type Request struct {
	// ToolCallID is the fantasy.ToolCall.ID for this invocation.
	ToolCallID string
	// Questions is the validated question set.
	Questions []Question
}

Request is what the tool hands to a Resolver. It carries the model-supplied questions plus the upstream tool-call ID so hosts that route through a pubsub broker or stream-json child can correlate the reply.

type Resolver

type Resolver interface {
	Ask(ctx context.Context, req Request) ([]Answer, error)
}

Resolver is the host-supplied seam that actually asks the user. The library validates the model's input, builds a Request, and hands it to Ask. The Resolver returns one Answer per Question (in the same order) or an error.

Implementations should:

  • respect ctx cancellation (e.g. the user closed the session)
  • return ErrResolverCancelled when the user dismisses/supersedes
  • never return more answers than there are questions

type ResolverFunc

type ResolverFunc func(ctx context.Context, req Request) ([]Answer, error)

ResolverFunc adapts a plain function into a Resolver.

func (ResolverFunc) Ask

func (f ResolverFunc) Ask(ctx context.Context, req Request) ([]Answer, error)

Ask implements Resolver.

type StaticResolver

type StaticResolver struct {
	Answers []Answer
	Err     error
}

StaticResolver returns the same canned answers for every call. Useful for tests, dry runs, and golden-file snapshots.

func (StaticResolver) Ask

func (s StaticResolver) Ask(_ context.Context, _ Request) ([]Answer, error)

Ask implements Resolver.

type StdinResolver

type StdinResolver struct {
	In  io.Reader
	Out io.Writer
}

StdinResolver is a minimal CLI resolver: it prints each question to Out and reads selections from In. Options are numbered starting at 1; the user types the number, a comma-separated list of numbers for multi-select, or the literal word "other" followed by free text. Empty input cancels.

This resolver exists primarily for examples and headless smoke tests. Production hosts should implement a richer UI (TUI modal, web form, etc.).

func (StdinResolver) Ask

func (s StdinResolver) Ask(ctx context.Context, req Request) ([]Answer, error)

Ask implements Resolver.

Directories

Path Synopsis
examples
cli command
Command askuserquestion-demo wires the askuserquestion tool to stdin/stdout so you can see the full request/answer loop without a real model.
Command askuserquestion-demo wires the askuserquestion tool to stdin/stdout so you can see the full request/answer loop without a real model.

Jump to

Keyboard shortcuts

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