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
- Variables
- func Digest(tool string, input json.RawMessage) string
- func Match(e hookevents.Event) bool
- type Answer
- type Ask
- type BadRequestError
- type Decision
- type Message
- type Option
- type Question
- type Reply
- type Request
- type Row
- type Sessions
- type Settings
- type Store
- func (s *Store) Answer(id string, reply Reply) error
- func (s *Store) Ask(ctx context.Context, ask Ask) (Decision, error)
- func (s *Store) DeleteMessage(agent, id string)
- func (s *Store) Handler() http.Handler
- func (s *Store) List() ([]Request, []Message)
- func (s *Store) Methods() map[string]daemon.Method
- func (s *Store) Observe(e hookevents.Event)
- func (s *Store) Queue(agent, id, text string) error
- func (s *Store) Requeue(m Message)
- func (s *Store) Run(ctx context.Context, events <-chan hookevents.Record)
- func (s *Store) SetSettings(settings Settings) (Settings, error)
- func (s *Store) Settings() Settings
- func (s *Store) TakeMessage(agent, id string) (Message, bool)
- func (s *Store) Waits() map[string]daemon.Method
- type Suggestion
- type TakeParams
- type Taken
Constants ¶
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.
const ( AgentClaudeCode = "claude-code" AgentCodex = "codex" )
Agents whose requests the app answers.
const ( KindPermission = "permission" // KindQuestion is Claude Code's AskUserQuestion. KindQuestion = "question" )
Kinds of request.
const ( BehaviorAllow = "allow" BehaviorDeny = "deny" )
Behaviours of a decision.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
DeleteMessage removes the message queued for a session, if any.
func (*Store) 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 ¶
List is the pending requests, oldest first, and the queued messages of sessions still listed, with their text masked.
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 ¶
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 ¶
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 ¶
SetSettings saves new settings. Turning Codex approvals off withdraws the Codex requests waiting, which then fall back to Codex's own prompt.
func (*Store) TakeMessage ¶
TakeMessage removes and returns the message queued for a session.
type Suggestion ¶
Suggestion is a permission suggestion; its ID is its index.
type TakeParams ¶
TakeParams name the session whose message MethodTakeMessage takes.