Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,14 @@ In view mode, use =r= to compose a reply, =R= to quote the response in a reply,
Do not invoke =agent-shell-viewport-edit-mode= or =agent-shell-viewport-view-mode= directly in an agent shell buffer. They are major modes installed by the entry points above on the separate viewport buffer, not commands for converting the shell buffer.
#+end_quote

*** Steering a running turn

A prompt sent while the agent is working is queued and submitted once the turn ends. Agents that support steering can take it into the turn already running instead, so a wrong direction can be corrected without waiting for it to finish. Steering is an ACP extension (=_session/steering=) rather than part of the spec, advertised by the agent at initialization: the Claude and Codex packages implement it, other agents keep queueing.

Sending from a busy shell steers automatically when the agent supports it. Use a prefix argument (=C-u=) to queue that one prompt instead, or set =agent-shell-steer-when-busy= to nil to always queue. =M-x agent-shell-steer= steers explicitly, and errors when there is no running turn to steer or the agent cannot steer. A steer that the agent does not accept falls back to queueing, so the prompt is never lost.

What the agent does with the steered prompt is up to the agent: Claude interrupts what it is generating and responds to it, while Codex finishes the message in flight first.

*** Specific Agent Commands

Start a specific agent shell session directly:
Expand Down Expand Up @@ -982,6 +990,7 @@ always go to Evil modes if you need to with ~C-z~).
| agent-shell-show-context-usage-indicator | Non-nil to show the context usage indicator in the header and mode line. |
| agent-shell-show-usage-at-turn-end | Whether to display usage information when agent turn ends. |
| agent-shell-show-welcome-message | Non-nil to show welcome message. |
| agent-shell-steer-when-busy | Whether a prompt sent mid-turn steers the agent instead of queueing. |
| agent-shell-text-file-capabilities | Whether agents are initialized with read/write text file capabilities. |
| agent-shell-thought-process-expand-by-default | Whether thought process sections should be expanded by default. |
| agent-shell-thought-process-icon | Icon displayed during the AI’s thought process. |
Expand Down Expand Up @@ -1067,7 +1076,7 @@ always go to Evil modes if you need to with ~C-z~).
| p or <backtab> | agent-shell-previous-item | Go to previous item. |
| | agent-shell-previous-permission-button | Jump to the previous button. |
| | agent-shell-prompt-compose | Compose an `agent-shell' prompt in a dedicated buffer. |
| | agent-shell-prompt-queue | Queue or immediately send a prompt depending on shell busy state. |
| | agent-shell-prompt-queue | Steer, queue, or immediately send a prompt depending on shell state. |
| | agent-shell-prompt-queue-remove | Remove all pending prompts or a specific prompt by REMOVE-INDEX. |
| | agent-shell-prompt-queue-resume | Resume processing pending prompts in the queue. |
| r | agent-shell-quote-region | Quote the active region into the shell's latest prompt. |
Expand All @@ -1084,6 +1093,7 @@ always go to Evil modes if you need to with ~C-z~).
| | agent-shell-send-screenshot | Capture a screenshot and insert it into `agent-shell'. |
| C-c RET | agent-shell-set-session-mode | Set session mode (if any available). |
| C-c C-v | agent-shell-set-session-model | Set session model. |
| | agent-shell-steer | Steer PROMPT into the turn the agent is currently running. |
| RET | agent-shell-submit | Submit current input. |
| | agent-shell-toggle | Toggle agent shell display. |
| | agent-shell-toggle-logging | Toggle logging. |
Expand Down
113 changes: 113 additions & 0 deletions agent-shell-experimental.el
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
;; a request to the client, followed by session/update notifications,
;; concluded by an session_push_end notification. The client
;; then responds to the original request.
;;
;; _session/steering: Steer a prompt into the turn already running,
;; instead of queueing it until the turn ends. The client sends the
;; request; the agent answers whether the prompt joined the running
;; turn, or why it could not. The steered prompt's own output arrives
;; as ordinary session/update notifications on the turn already in
;; flight, so nothing else changes.

;;; Code:

Expand All @@ -37,6 +44,9 @@

(declare-function acp-send-response "acp")
(declare-function acp-make-error "acp")
(declare-function agent-shell--build-content-blocks "agent-shell")
(declare-function agent-shell--expand-truncated-regions "agent-shell")
(declare-function agent-shell--send-request "agent-shell")
(declare-function agent-shell-heartbeat-start "agent-shell-heartbeat")
(declare-function agent-shell-heartbeat-stop "agent-shell-heartbeat")
(declare-function shell-maker-insert-end-of-prompt-marker "shell-maker")
Expand Down Expand Up @@ -136,6 +146,109 @@ ERROR is an optional error object if the push prompt was rejected."
"Return the list of experimental methods that replay session notifications."
'("session/push"))

(defconst agent-shell-experimental--steering-method "_session/steering"
"Request method that steers a prompt into the turn already running.

Not part of the ACP spec -- the leading underscore marks it as an
extension -- but implemented under this name by the Claude and Codex
adapters, which advertise it as `_meta.steering.supported' in their
`initialize' response. The ACP proposal that would standardise this is
`session/inject' (RFD 1261), still unmerged and targeting v2.")

(cl-defun agent-shell-experimental--make-session-steering-request (&key session-id prompt)
"Instantiate a steering request for SESSION-ID carrying PROMPT.

PROMPT is a vector of content blocks, the same shape `session/prompt'
takes.

The `idleBehavior' opt-in asks the agent to do nothing and say so when
no turn is running, rather than starting a detached turn out of our
sight. Only the Claude adapter honours it; Codex's parameter parser
passes unknown keys through and ignores them, so a steer that races the
end of a turn can still come back `startedNewTurn' there.

For example:

(agent-shell-experimental--make-session-steering-request
:session-id \"sess-1\"
:prompt [((type . \"text\") (text . \"just the filenames\"))])

=> ((:method . \"_session/steering\")
(:params . ((sessionId . \"sess-1\")
(prompt . [((type . \"text\")
(text . \"just the filenames\"))])
(_meta . ((steering
. ((idleBehavior . \"promptRequired\"))))))))"
(unless session-id
(error ":session-id is required"))
(unless prompt
(error ":prompt is required"))
`((:method . ,agent-shell-experimental--steering-method)
(:params . ((sessionId . ,session-id)
(prompt . ,(vconcat prompt))
(_meta . ((steering . ((idleBehavior . "promptRequired")))))))))

(defun agent-shell-experimental--steering-outcome (acp-response)
"Return ACP-RESPONSE's steering outcome as a symbol.

One of:

`injected' - the prompt joined the turn that was running.
`prompt-required' - no turn was running and the agent left the
prompt with us to submit normally.
`started-new-turn' - no turn was running and the agent started one
of its own, which we did not ask for and cannot
track.
`failed' - the agent could not apply the steer.

An unrecognised outcome maps to `failed' so a future agent's new answer
falls back to queueing rather than being mistaken for success.

For example:

(agent-shell-experimental--steering-outcome \\='((outcome . \"injected\")))
=> injected

(agent-shell-experimental--steering-outcome \\='((outcome . \"whatever\")))
=> failed"
(pcase (map-elt acp-response 'outcome)
("injected" 'injected)
("promptRequired" 'prompt-required)
("startedNewTurn" 'started-new-turn)
(_ 'failed)))

(cl-defun agent-shell-experimental--send-steering (&key state prompt on-outcome)
"Steer PROMPT into the turn STATE's session is currently running.

Must be called from the shell buffer: PROMPT is converted with
`agent-shell--build-content-blocks', which reads the buffer's prompt
capabilities.

ON-OUTCOME is called with (OUTCOME MESSAGE): OUTCOME as per
`agent-shell-experimental--steering-outcome', and MESSAGE the agent's
error text when the request itself failed, nil otherwise. A failed
request reports `failed' rather than propagating the error, so callers
have one code path for \"this did not get steered\" and can fall back to
queueing without losing the prompt."
(let* ((expanded (agent-shell--expand-truncated-regions prompt))
(content-blocks (condition-case nil
(agent-shell--build-content-blocks expanded)
(error `[((type . "text")
(text . ,(substring-no-properties expanded)))]))))
(agent-shell--send-request
:state state
:client (map-elt state :client)
:request (agent-shell-experimental--make-session-steering-request
:session-id (map-nested-elt state '(:session :id))
:prompt content-blocks)
:buffer (current-buffer)
:on-success (lambda (acp-response)
(funcall on-outcome
(agent-shell-experimental--steering-outcome acp-response)
nil))
:on-failure (lambda (acp-error _raw-message)
(funcall on-outcome 'failed (map-elt acp-error 'message))))))

(defun agent-shell-experimental--normalize-request (request)
"Normalize REQUEST from JSON symbol keys to keyword keys.

Expand Down
145 changes: 137 additions & 8 deletions agent-shell-prompt-queue.el
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,35 @@
(declare-function agent-shell--shell-buffer "agent-shell")
(declare-function agent-shell--state "agent-shell")
(declare-function agent-shell--echo "agent-shell")
(declare-function agent-shell--active-requests-p "agent-shell")
(declare-function agent-shell--emit-event "agent-shell")
(declare-function agent-shell--render-steered-prompt "agent-shell")
(declare-function agent-shell-status "agent-shell")
(declare-function agent-shell-steering-supported-p "agent-shell")
(declare-function agent-shell-experimental--send-steering "agent-shell-experimental")
(declare-function agent-shell-completion--setup-minibuffer "agent-shell-completion")
(declare-function shell-maker-busy "shell-maker")

(defvar agent-shell--state)
(defvar comint-input-ring)

(defcustom agent-shell-steer-when-busy t
"Whether a prompt sent mid-turn steers the agent instead of queueing.

Steering hands the prompt to the agent while it is still working, so it
can change course. Queueing holds the prompt until the turn ends and
then sends it as a new one.

Only agents that advertise steering can be steered; the rest queue
regardless. A prefix argument to `agent-shell-prompt-queue' forces
queueing for one prompt without changing this.

Note that steering may cut the agent off mid-answer: whether the
in-flight response is interrupted or the prompt waits for a safe
break-point is the agent's choice, not ours."
:type 'boolean
:group 'agent-shell)

;; The queueing commands were renamed to the `agent-shell-prompt-queue'
;; namespace. A package upgrade reloads this file into a running session
;; (see `package--reload-previously-loaded'), which redefines the new
Expand Down Expand Up @@ -205,24 +229,129 @@ agent commands when the agent has reported them."
(read-string (or (map-nested-elt (agent-shell--state) '(:agent-config :shell-prompt))
"Enqueue prompt: ")))))

(defun agent-shell-prompt-queue (prompt)
"Queue or immediately send a prompt depending on shell busy state.
(defun agent-shell--prompt-queue-steer-p ()
"Return non-nil when a prompt sent now should steer the running turn.

Requires `agent-shell-steer-when-busy', an agent that advertises
steering, and a turn that is running but not `blocked'. A blocked shell
is waiting on a permission answer: what an agent does with a message
injected while a tool sits on that question is left undefined by every
implementation, so those prompts keep queueing."
(and agent-shell-steer-when-busy
(agent-shell-steering-supported-p)
(eq (agent-shell-status) 'busy)))

(cl-defun agent-shell--prompt-queue-steer (&key prompt)
"Steer PROMPT into the running turn, queueing it if that fails.

Dispatches on the agent's answer:

`injected' renders PROMPT as a user prompt in the running turn.
`prompt-required' the turn had ended and the agent left PROMPT with
us, so submit it as an ordinary prompt.
`started-new-turn' the turn had ended and the agent started one we did
not ask for; its output arrives with no request in
flight, so say so rather than let it appear as an
unexplained out-of-turn message.
`failed' queue PROMPT unchanged.

Every path keeps PROMPT: a steer that does not land must not cost the
user their text."
(let ((state (agent-shell--state)))
(agent-shell-experimental--send-steering
:state state
:prompt prompt
:on-outcome
(lambda (outcome message)
(pcase outcome
('injected
(agent-shell--render-steered-prompt :state state :prompt prompt))
('prompt-required
;; The agent saw no turn, but this shell may not have processed
;; its own `session/prompt' response yet, and submitting into a
;; still-busy shell errors and drops the text. Queueing is
;; correct either way: the queue drains as soon as the turn
;; settles.
(if (shell-maker-busy)
(agent-shell--prompt-queue-enqueue :prompt prompt)
(agent-shell--insert-to-shell-buffer :text prompt :submit t :no-focus t)))
('started-new-turn
(agent-shell--update-fragment
:state state
:block-id (format "%s-steer-detached-turn"
(map-elt state :request-count))
:label-left (propertize "Steered prompt started a new turn"
'font-lock-face 'agent-shell-section-heading)
:body (format "The turn ended before this prompt reached the agent, so
the agent started one of its own for it:

%s

Its output arrives out of turn, and the shell does not show as
busy while it runs." prompt)
:create-new t
:above-last-prompt (not (agent-shell--active-requests-p state))))
(_
(agent-shell--prompt-queue-enqueue :prompt prompt)
(when message
(agent-shell--echo "Steering failed (%s); prompt queued" message))))
(agent-shell--emit-event :event 'prompt-steered
:data (list (cons :prompt prompt)
(cons :outcome outcome)))))))

(defun agent-shell-prompt-queue (prompt &optional queue-only)
"Steer, queue, or immediately send a prompt depending on shell state.

Read PROMPT from the minibuffer and act on the current project's shell,
resolving it via `agent-shell--shell-buffer' so this works even when
invoked outside a shell buffer. If the shell is busy, add PROMPT to the
pending prompts queue. Otherwise, submit it immediately. Queued prompts
will be automatically sent when the current prompt completes.
invoked outside a shell buffer.

When the shell is idle, submit PROMPT immediately. When a turn is
running, steer PROMPT into it if the agent supports that (see
`agent-shell--prompt-queue-steer-p'), so the agent can change course
rather than finish first. Otherwise add PROMPT to the pending prompts
queue, which is sent automatically when the current turn completes.

With a prefix argument, or with QUEUE-ONLY non-nil, always queue rather
than steer -- for when the agent should finish what it is doing before
reading the next thing.

While reading, @ completes project files and / completes available agent
commands when the agent has reported them."
(interactive
(list (with-current-buffer (agent-shell--shell-buffer :no-create t)
(agent-shell--prompt-queue-read))
current-prefix-arg))
(with-current-buffer (agent-shell--shell-buffer :no-create t)
(cond
((not (shell-maker-busy))
(agent-shell--insert-to-shell-buffer :text prompt :submit t :no-focus t))
((and (not queue-only) (agent-shell--prompt-queue-steer-p))
(agent-shell--prompt-queue-steer :prompt prompt))
(t
(agent-shell--prompt-queue-enqueue :prompt prompt)))))

(defun agent-shell-steer (prompt)
"Steer PROMPT into the turn the agent is currently running.

Unlike queueing, the prompt reaches the agent while it works, so it can
change course instead of finishing first. Signals a `user-error' when
the agent does not support steering or no turn is running -- use
`agent-shell-prompt-queue' for those.

While reading, @ completes project files and / completes available agent
commands when the agent has reported them."
(interactive
(list (with-current-buffer (agent-shell--shell-buffer :no-create t)
(agent-shell--prompt-queue-read))))
(with-current-buffer (agent-shell--shell-buffer :no-create t)
(if (shell-maker-busy)
(agent-shell--prompt-queue-enqueue :prompt prompt)
(agent-shell--insert-to-shell-buffer :text prompt :submit t :no-focus t))))
(unless (shell-maker-busy)
(user-error "No turn to steer; the agent is idle"))
(unless (agent-shell-steering-supported-p)
(user-error "This agent does not support steering"))
(when (eq (agent-shell-status) 'blocked)
(user-error "Answer the pending permission request first"))
(agent-shell--prompt-queue-steer :prompt prompt)))

(defun agent-shell-prompt-queue-resume ()
"Resume processing pending prompts in the queue.
Expand Down
Loading
Loading