Wake agents when the human runs a command they left in the terminal

This commit is contained in:
Ian Gulliver
2026-10-19 21:44:46 -07:00
parent f6f2440635
commit 5c714b3e2b
4 changed files with 112 additions and 8 deletions
+29 -2
View File
@@ -5,6 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"sync/atomic"
"time"
@@ -53,6 +55,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool {
Description: "Type into your task's terminal: the shell in this task's worktree that the human uses from the Terminal tab, starting it when none is running and waiting for its prompt before typing. " +
"text goes to the shell exactly as keystrokes, so end a command with \"\\n\" to run it, and send control characters for keys (\"\\u0003\" is Ctrl-C). " +
"The human sees everything you type and its output. " +
"To hand the human a command to run (one needing their password, say), type it without the newline and tell them to press Enter in the Terminal tab. In a zsh terminal, the reply then says byre will message you when the shell next returns to its prompt, with the exit status and output, starting a turn if you are idle, so end your turn rather than asking whether they ran it. " +
"Returns the output that followed, as terminal_read does, once the terminal has been quiet for half a second or after 10 seconds; read the rest of a longer command's output with terminal_read.",
InputSchema: json.RawMessage(`{
"type": "object",
@@ -78,12 +81,33 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool {
if err != nil {
return "", err
}
return typeAndWait(ctx, term, args.Text)
if args.Text == "" || strings.HasSuffix(args.Text, "\n") || strings.HasSuffix(args.Text, "\r") {
return typeAndWait(ctx, term, args.Text, nil)
}
if !term.Prompts {
out, err := typeAndWait(ctx, term, args.Text, nil)
return out + "\n[this shell isn't zsh, so byre can't tell when the line is run: ask the human to tell you once they have run it]", err
}
out, err := typeAndWait(ctx, term, args.Text, m.wakeOnPrompt(l))
return out + "\n[byre messages you when the shell is next back at its prompt]", err
},
}
}
func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (string, error) {
func (m *Manager) wakeOnPrompt(l *live) func(string, []byte) {
return func(exit string, out []byte) {
text := fmt.Sprintf("The line you left unsent in the terminal was run, and the shell is back at its prompt with exit status %s (130 means it was cancelled with Ctrl-C). The terminal's output since you typed it:\n\n%s", exit, terminal.Text(out))
_, _, err := m.Message(context.Background(), l.id, "byre", text, "")
if err != nil {
slog.Error("wake agent on terminal prompt", "session", l.id, "task", l.taskID, "err", err)
return
}
slog.Info("woke agent on terminal prompt", "session", l.id, "task", l.taskID, "exit", exit)
}
}
func typeAndWait(ctx context.Context, term *terminal.Terminal, text string, wake func(string, []byte)) (string, error) {
term.CancelWake()
backlog, out := term.Subscribe()
if out == nil {
exit, _ := term.ExitStatus()
@@ -114,6 +138,9 @@ func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (str
}
typed.Add(1)
}
if wake != nil && typed.Load() == int64(len(text)) {
term.WakeOnPrompt(wake)
}
written <- nil
}()