agentrequests

package
v0.1.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package agentrequests lets the desktop app answer what a coding agent asks its user, and send a running session a message. A Claude Code or Codex PermissionRequest hook describes the request to the daemon over the control socket (MethodRequest) and holds until the app answers, the request is withdrawn or the hook goes away; a Stop hook takes the message queued for its session (MethodTakeMessage) at once.

The hook keeps the agent's own payload and builds the agent's answer from it: the daemon is told only what the app shows (rows, suggestion labels and questions), masks every string as transcripts are masked, and answers with a behaviour, a suggestion index and the chosen options. Requests and messages live in memory only, are never persisted or logged, and never leave the machine; only Settings is kept, in the daemon's state directory.

Index

Constants

View Source
const (
	// MethodRequest is a waiting control request: its params are an Ask, its
	// result the Decision, which is empty when nobody decided.
	MethodRequest = "agent.request"
	// MethodTakeMessage answers at once with the message queued for a
	// session (TakeParams, Taken) and removes it.
	MethodTakeMessage = "agent.take-message"
)

Control requests a hook sends.

View Source
const (
	AgentClaudeCode = "claude-code"
	AgentCodex      = "codex"
)

Agents whose requests the app answers.

View Source
const (
	KindPermission = "permission"
	// KindQuestion is Claude Code's AskUserQuestion.
	KindQuestion = "question"
)

Kinds of request.

View Source
const (
	BehaviorAllow = "allow"
	BehaviorDeny  = "deny"
)

Behaviours of a decision.

View Source
const (
	// MaxValue bounds a row's value, which may keep its newlines.
	MaxValue = 4000
	// MaxMessage bounds a queued message.
	MaxMessage = 4000

	MaxQuestions = 8
	MaxOptions   = 16
	// MaxSuggestions bounds the suggestions shown; a hook sends the first
	// ones, so an index stays the agent's own.
	MaxSuggestions = 16

	// DefaultCodexWait is how long a Codex request waits for the app before
	// the hook prints nothing and Codex asks in its terminal: the hook's
	// own timeout is 30 seconds.
	DefaultCodexWait = 25 * time.Second
	// ClaudeHold is the timeout of Claude Code's permission-request hook
	// (its agenthooks placement), after which Claude Code kills the hook.
	ClaudeHold = time.Hour
)

Bounds, in characters unless named otherwise.

Variables

View Source
var (
	// ErrGone is a request answered already, in the terminal or the app,
	// timed out, or withdrawn because its session moved on.
	ErrGone = errors.New("this request was already answered, timed out or its session moved on")
	// ErrNotRunning is a message for a session that is not working on a
	// turn, so no Stop hook would take it.
	ErrNotRunning = errors.New("the session is not running")
)

Errors of Answer and Queue.

Functions

func Digest

func Digest(tool string, input json.RawMessage) string

Digest identifies a tool call by its tool's name and input, so a request and the post-tool-use event of the same call match without either keeping the input: SHA-256 of the name and the input's canonical JSON (members sorted, numbers as written). An AskUserQuestion is identified by its questions alone, since the input it ran with carries the answers. It is empty for input that is not JSON.

func Match

func Match(e hookevents.Event) bool

Match reports whether Observe acts on an event; the daemon subscribes with it.

Types

type Answer

type Answer struct {
	Options []int  `json:"options,omitempty"`
	Text    string `json:"text,omitempty"`
}

Answer is the answer to one question: the indexes of the options chosen, or text the user wrote instead.

type Ask

type Ask struct {
	Agent     string `json:"agent"`
	SessionID string `json:"session_id"`
	// AgentID is the Claude Code subagent asking, empty for the session's
	// main agent: the main turn's boundaries leave its requests alone.
	AgentID string `json:"agent_id,omitempty"`
	Kind    string `json:"kind"`
	Tool    string `json:"tool"`
	// Digest is Digest of the tool's name and input, which the session's
	// post-tool-use (or post-tool-use-failure) event carries once the tool
	// ran.
	Digest string `json:"digest,omitempty"`
	// StartedAt is when the hook started: a turn boundary or a finished
	// tool call recorded since makes the request moot.
	StartedAt   time.Time  `json:"started_at"`
	Input       []Row      `json:"input,omitempty"`
	Suggestions []string   `json:"suggestions,omitempty"`
	Questions   []Question `json:"questions,omitempty"`
}

Ask is a hook's request: MethodRequest's params.

type BadRequestError

type BadRequestError struct{ Reason string }

BadRequestError is an answer or message the store refuses as given.

func (*BadRequestError) Error

func (e *BadRequestError) Error() string

type Decision

type Decision struct {
	Behavior string `json:"behavior,omitempty"`
	// Suggestion is the index of the permission suggestion to apply.
	Suggestion *int `json:"suggestion,omitempty"`
	// Answers holds one answer per question, in order.
	Answers []Answer `json:"answers,omitempty"`
}

Decision is the app's answer to an Ask; an empty Behavior decides nothing, and the hook prints nothing.

type Message

type Message struct {
	Agent     string    `json:"agent"`
	SessionID string    `json:"session_id"`
	Text      string    `json:"text"`
	QueuedAt  time.Time `json:"queued_at"`
}

Message is a message queued for a session's next Stop hook.

type Option

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

Option is one answer a question offers.

type Question

type Question struct {
	Question    string   `json:"question"`
	Header      string   `json:"header,omitempty"`
	MultiSelect bool     `json:"multi_select"`
	Options     []Option `json:"options"`
}

Question is one of the questions an AskUserQuestion request asks.

type Reply

type Reply struct {
	Behavior   string `json:"behavior"`
	Suggestion string `json:"suggestion,omitempty"`
	// Answers maps a question's text, as shown, to the label chosen (labels
	// joined by ", " for several) or to text the user wrote.
	Answers map[string]string `json:"answers,omitempty"`
}

Reply is the app's answer to a request.

type Request

type Request struct {
	ID          string       `json:"id"`
	Agent       string       `json:"agent"`
	SessionID   string       `json:"session_id"`
	Kind        string       `json:"kind"`
	Tool        string       `json:"tool"`
	CreatedAt   time.Time    `json:"created_at"`
	Input       []Row        `json:"input"`
	Suggestions []Suggestion `json:"suggestions"`
	Questions   []Question   `json:"questions"`
}

Request is a pending request as the app is shown it.

type Row

type Row struct {
	Label string `json:"label"`
	Value string `json:"value"`
	// Code renders the value monospace.
	Code bool `json:"code,omitempty"`
}

Row is one line of what a request would do, as the app shows it.

type Sessions

type Sessions interface {
	Lookup(agent, id string) (livesessions.Session, string, bool)
}

Sessions are the live sessions; *livesessions.Tracker is one.

type Settings

type Settings struct {
	// CodexApprovals lets the app answer Codex's permission requests. Codex
	// hides its own prompt while the hook waits, so it is off unless chosen.
	CodexApprovals bool `json:"codex_approvals"`
}

Settings are what the user chose.

type Store

type Store struct {
	// Sessions decides which sessions can be sent a message.
	Sessions Sessions
	// Mask masks what an agent wrote, as transcripts are masked; nil keeps
	// it.
	Mask func(string) string
	// Notify is called after the requests or messages changed. It must not
	// block.
	Notify func()
	// Database is the application database path; empty keeps settings in memory.
	Database string
	// CodexWait replaces DefaultCodexWait.
	CodexWait time.Duration
	// Interrupted, when set, is told of a Claude Code session whose main
	// agent's request ended with its hook well before ClaudeHold: Claude
	// Code kills the hook when the user turns the request down in the
	// terminal, which interrupts the turn without a stop.
	Interrupted func(agent, session string, at time.Time)
	// Answered, when set, is told of a session the app answered a request
	// of: it works again, though the agent reports nothing until the tool
	// call ends.
	Answered func(agent, session string, at time.Time)
	// Resumed, when set, is told of a session whose Stop hook was handed
	// the message queued for it: the agent goes on with its turn, though it
	// may report nothing until it calls a tool or stops again.
	Resumed func(agent, session string, at time.Time)
	// Now is the clock; nil is time.Now.
	Now func() time.Time
	Log *slog.Logger
	// contains filtered or unexported fields
}

Store holds the pending requests and queued messages.

func (*Store) Answer

func (s *Store) Answer(id string, reply Reply) error

Answer decides a pending request with the app's reply. A request that is no longer pending is ErrGone; a reply that does not fit it is a *BadRequestError.

func (*Store) Ask

func (s *Store) Ask(ctx context.Context, ask Ask) (Decision, error)

Ask shows a request to the app and waits until it is answered (the Decision), withdrawn, ctx ends or, for Codex, CodexWait passed; all but the first decide nothing. It decides nothing at once for a Codex request unless the user turned CodexApprovals on, and for a request that cannot be shown safely (newPending).

func (*Store) DeleteMessage

func (s *Store) DeleteMessage(agent, id string)

DeleteMessage removes the message queued for a session, if any.

func (*Store) Handler

func (s *Store) Handler() http.Handler

Handler serves the app's routes, which the daemon mounts behind the same authentication as every application route:

  • GET /api/agent-requests: {"requests","messages","settings"}, the pending requests oldest first and the queued messages;
  • POST /api/agent-requests/{id}/answer with a Reply: {"ok":true}, 409 request_gone for a request no longer pending, 400 bad_request for a reply that does not fit it;
  • PUT /api/agent-requests/settings with Settings: the settings saved;
  • POST /api/agent-messages with {"agent","session_id","text"}: queues it, 404 session_not_running for a session no Stop hook would come from;
  • DELETE /api/agent-messages/{agent}/{id}: removes a queued message.

func (*Store) List

func (s *Store) List() ([]Request, []Message)

List is the pending requests, oldest first, and the queued messages of sessions still listed, with their text masked.

func (*Store) Methods

func (s *Store) Methods() map[string]daemon.Method

Methods are the control requests the store answers at once.

func (*Store) Observe

func (s *Store) Observe(e hookevents.Event)

Observe applies a recorded hook event (Match): the main turn's boundaries of a session (user-prompt-submit, stop) withdraw its main agent's requests, a subagent's stop that subagent's, the session's end all of them, and a finished or failed tool call (post-tool-use, post-tool-use-failure) the request for the same call, the oldest when several match. Only requests whose hooks started before the event are withdrawn, so a late event never withdraws a newer request. A new prompt and the session's end also drop the message queued before them, which belonged to the turn that is over; a stop leaves it, since the Stop hook takes it after recording its event.

func (*Store) Queue

func (s *Store) Queue(agent, id, text string) error

Queue queues a message for the session's next Stop hook, replacing one queued before. The text must not be blank or longer than MaxMessage; the session must be working on a turn (ErrNotRunning otherwise).

func (*Store) Requeue

func (s *Store) Requeue(m Message)

Requeue puts back a message taken but not delivered, unless a newer one was queued since or its turn ended meanwhile (Observe saw a later prompt or the session's end while it was out of the queue).

func (*Store) Run

func (s *Store) Run(ctx context.Context, events <-chan hookevents.Record)

Run observes events until ctx ends or the channel closes.

func (*Store) SetSettings

func (s *Store) SetSettings(settings Settings) (Settings, error)

SetSettings saves new settings. Turning Codex approvals off withdraws the Codex requests waiting, which then fall back to Codex's own prompt.

func (*Store) Settings

func (s *Store) Settings() Settings

Settings are the current settings.

func (*Store) TakeMessage

func (s *Store) TakeMessage(agent, id string) (Message, bool)

TakeMessage removes and returns the message queued for a session.

func (*Store) Waits

func (s *Store) Waits() map[string]daemon.Method

Waits are the control requests the store holds (daemon.Options.Waits).

type Suggestion

type Suggestion struct {
	ID    string `json:"id"`
	Label string `json:"label"`
}

Suggestion is a permission suggestion; its ID is its index.

type TakeParams

type TakeParams struct {
	Agent     string `json:"agent"`
	SessionID string `json:"session_id"`
}

TakeParams name the session whose message MethodTakeMessage takes.

type Taken

type Taken struct {
	Text string `json:"text,omitempty"`
}

Taken answers MethodTakeMessage; Text is empty when none was queued.

Jump to

Keyboard shortcuts

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