agentsend

package
v0.3.31 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package agentsend delivers a prompt to a task's live agent.

Every surface that talks to a running agent — the TUI's retry-with-feedback, the web API's task input, the browser-annotation nudge, `ty input` — used to type into tmux itself, and each one had the same two holes:

  • It aimed at the pane id stored on the task row. tmux reuses pane ids, so a row whose agent has since gone away can name a pane belonging to another task — which has cross-wired two tasks onto one pane before. The pane is found here by asking tmux which pane carries the task's tag (tmuxctl.PaneTaskOption), and a task with no tagged pane is refused rather than typed at.
  • It sent the text and its Enter as two unsynchronised calls, so two nudges arriving together interleaved into one garbled line, and multi-line text submitted itself a line at a time. Here the text goes in as one bracketed paste and the Enter follows it under a per-pane lock.

Index

Constants

This section is empty.

Variables

View Source
var ErrBusy = errors.New("agent busy")

ErrBusy matches any BusyError under errors.Is.

View Source
var ErrNoPane = errors.New("no live agent pane")

ErrNoPane matches any NoPaneError under errors.Is.

Functions

func TaggedPane

func TaggedPane(runner Runner, taskID int64, role string) (string, error)

TaggedPane asks tmux which pane on the agent server carries this task's tag in the given role. Every pane on the server is listed, not just the ones in the task's window: the window may have been renamed, moved between daemon generations, or joined into a view, and the tag survives all three.

Types

type BusyError

type BusyError struct {
	TaskID int64
	Status string
}

BusyError means the agent is mid-turn. Surfaces show this to the user rather than dropping the text into a pane that is already producing output, and can resend with Force when interrupting is the point.

func (*BusyError) Error

func (e *BusyError) Error() string

func (*BusyError) Is

func (e *BusyError) Is(target error) bool

type NoPaneError

type NoPaneError struct{ TaskID int64 }

NoPaneError means no pane on the agent server carries this task's tag: its agent is not running, or is running somewhere this process cannot see. The stored pane id is deliberately NOT used as a fallback — whatever it names now is not known to be this task's agent.

func (*NoPaneError) Error

func (e *NoPaneError) Error() string

func (*NoPaneError) Is

func (e *NoPaneError) Is(target error) bool

type Prompt

type Prompt struct {
	TaskID int64
	// Text is pasted as-is. Newlines and shell metacharacters survive: it
	// travels as a tmux buffer, never as key names.
	Text string
	// Force sends even while the agent is working. For surfaces that interrupt
	// on purpose — a user who has just been told the agent is busy and said to
	// send anyway.
	Force bool
	// Submit presses Enter after the paste. False leaves the text sitting in the
	// agent's input box, which is what `ty input --no-submit` is for.
	Submit bool
}

Prompt is one delivery.

type Runner

type Runner interface {
	Run(name string, args ...string) error
	Output(name string, args ...string) ([]byte, error)
}

Runner runs a command. It is deliberately the same shape as web.CommandRunner, so the web server hands over the runner it already has — one that knows which tmux server TaskYou's agents live on — and its tests hand over the fake they already have.

type Sender

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

Sender delivers prompts to agents. The zero value is unusable; see New.

func New

func New(runner Runner, store Store) *Sender

New returns a Sender for this machine's agent server. store may be nil, which disables the busy check and the turn wait — for callers that have a tmux runner and nothing else.

func NewForHost

func NewForHost(runner Runner, store Store, host string) *Sender

NewForHost returns a Sender whose runner reaches another machine's tmux, for a task placed on a remote host. Its panes are resolved there by whoever holds the connection (see SendToPane); everything else — the busy check, the single paste, the lock — is the same.

func (*Sender) AgentPane

func (s *Sender) AgentPane(taskID int64) (string, error)

AgentPane returns the pane tagged as this task's agent, or *NoPaneError.

func (*Sender) Send

func (s *Sender) Send(p Prompt) error

Send delivers p to the task's tagged agent pane.

Returns *NoPaneError when no pane carries the task's tag, and *BusyError when the agent is working and Force is not set.

func (*Sender) SendAndWait

func (s *Sender) SendAndWait(ctx context.Context, p Prompt, timeout time.Duration) (db.AgentTurn, error)

SendAndWait delivers p and waits for the agent's answer to it.

The turn counters are read BEFORE the prompt goes out, so the Stop hook of whatever the agent was doing a moment ago cannot be mistaken for the reply: only a turn that starts after this send, and then finishes, counts. Returns db.ErrReplyTimeout if the agent does not answer in time — the prompt was still delivered.

func (*Sender) SendKeys

func (s *Sender) SendKeys(taskID int64, keys ...string) error

SendKeys presses tmux key names in the task's tagged agent pane — "Enter", "Escape", "Up". Keystrokes are not prompts: they are how a human answers a menu the agent is showing, so there is no busy check. The pane is still resolved by tag, because a keypress into a stranger's pane is no better than a prompt into one.

func (*Sender) SendKeysToPane

func (s *Sender) SendKeysToPane(pane string, keys ...string) error

SendKeysToPane is SendKeys for a pane the caller has already resolved. See SendToPane.

func (*Sender) SendToPane

func (s *Sender) SendToPane(pane string, p Prompt) error

SendToPane is Send for a pane the caller has already resolved. It exists for remote tasks, whose pane lives on another machine's tmux server and is found by the code that owns that connection — the busy check, the single paste and the per-pane lock are the same, so a remote task gets the same guarantees as a local one instead of its own hand-rolled send.

func (*Sender) SendToPaneAndWait

func (s *Sender) SendToPaneAndWait(ctx context.Context, pane string, p Prompt, timeout time.Duration) (db.AgentTurn, error)

SendToPaneAndWait is SendAndWait for a pane the caller has already resolved. See SendToPane.

type Store

type Store interface {
	GetTask(id int64) (*db.Task, error)
	AgentTurnState(taskID int64) (db.AgentTurn, error)
	WaitForAgentReply(ctx context.Context, taskID int64, since db.AgentTurn, timeout time.Duration) (db.AgentTurn, error)
}

Store is the slice of the database this package needs. An interface so a test can drive the busy check and the turn wait without a real database, and so it is obvious that nothing here writes to the task row.

Jump to

Keyboard shortcuts

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