Skip to content
Open
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
1,353 changes: 1,353 additions & 0 deletions acp.el

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions agent-shell-openai.el
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,16 @@ Returns an agent configuration alist using `agent-shell-make-agent-config'."
agent-shell-openai-default-model-id))
:default-session-mode-id (lambda () agent-shell-openai-default-session-mode-id)
:authenticate-request-maker (lambda ()
(cond ((map-elt agent-shell-openai-authentication :api-key)
(acp-make-authenticate-request :method-id "openai-api-key"))
((map-elt agent-shell-openai-authentication :codex-api-key)
(acp-make-authenticate-request :method-id "codex-api-key"))
(cond ((or (map-elt agent-shell-openai-authentication :api-key)
(map-elt agent-shell-openai-authentication :codex-api-key))
(let ((api-key (agent-shell-openai-key)))
(unless api-key
(user-error "Please set your `agent-shell-openai-authentication'"))
(acp-make-authenticate-request
:method-id "api-key"
:meta `((api-key . ((apiKey . ,api-key)))))))
(t
(acp-make-authenticate-request :method-id "chatgpt"))))
(acp-make-authenticate-request :method-id "chat-gpt"))))
:client-maker (lambda (buffer)
(agent-shell-openai-make-codex-client :buffer buffer))
:install-instructions "See https://github.com/zed-industries/codex-acp for installation."))
Expand Down Expand Up @@ -185,14 +189,13 @@ Uses `agent-shell-openai-authentication' for authentication configuration."
(user-error "Please set your `agent-shell-openai-authentication'"))
(agent-shell--make-acp-client :command (car agent-shell-openai-codex-acp-command)
:command-params (cdr agent-shell-openai-codex-acp-command)
:environment-variables (append (list (format "CODEX_API_KEY=%s" codex-key))
:environment-variables (append (list (format "OPENAI_API_KEY=%s" codex-key))
agent-shell-openai-codex-environment)
:context-buffer buffer)))
((map-elt agent-shell-openai-authentication :login)
(agent-shell--make-acp-client :command (car agent-shell-openai-codex-acp-command)
:command-params (cdr agent-shell-openai-codex-acp-command)
:environment-variables (append '("OPENAI_API_KEY=")
agent-shell-openai-codex-environment)
:environment-variables agent-shell-openai-codex-environment
:context-buffer buffer))
(t
(error "Invalid authentication configuration"))))
Expand Down
22 changes: 17 additions & 5 deletions agent-shell-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,7 @@ O(accumulated-body). Label-only updates leave the body untouched."
(let* ((state (get-text-property (prop-match-beginning match)
'agent-shell-ui-state))
(collapsed (map-elt state :collapsed))
(existing-body-range
(agent-shell-ui--nearest-range-matching-property
:property 'agent-shell-ui-section :value 'body
:from (prop-match-beginning match)
:to (prop-match-end match))))
(existing-body-range nil))
(setq block-start (prop-match-beginning match))
(save-excursion
(goto-char block-start)
Expand All @@ -121,6 +117,22 @@ O(accumulated-body). Label-only updates leave the body untouched."
(when new-label-right
(agent-shell-ui--replace-label
qualified-id 'label-right new-label-right))
;; Derive the body range *after* the label edits.
;; `agent-shell-ui--replace-label' can change a label's
;; length (e.g. the status label growing from "pending"
;; to "completed"), shifting every position to its right.
;; A range captured before the edits would be stale and
;; make `--replace-body' delete the wrong region, eating
;; the label↔body boundary and leaving the body visible
;; under a collapsed `▶' indicator.
(setq existing-body-range
(when-let* ((block-range
(agent-shell-ui--block-range
:position block-start)))
(agent-shell-ui--nearest-range-matching-property
:property 'agent-shell-ui-section :value 'body
:from (map-elt block-range :start)
:to (map-elt block-range :end))))
(when new-body
(cond
;; Append to existing body — preserves rendered content.
Expand Down
91 changes: 83 additions & 8 deletions agent-shell.el
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,20 @@ Sources are checked in order until one returns non-nil."
COMMAND, COMMAND-PARAMS, ENVIRONMENT-VARIABLES, and CONTEXT-BUFFER are
passed through to `acp-make-client'."
(let* ((full-command (append (list command) command-params))
(wrapped-command (agent-shell--build-command-for-execution full-command)))
(wrapped-command (agent-shell--build-command-for-execution full-command))
(agent-identifier (when context-buffer
(map-nested-elt
(buffer-local-value 'agent-shell--state context-buffer)
'(:agent-config :identifier))))
(agent-name (cond
((stringp agent-identifier) agent-identifier)
((symbolp agent-identifier) (symbol-name agent-identifier))
(t nil))))
(acp-make-client :command (car wrapped-command)
:command-params (cdr wrapped-command)
:environment-variables environment-variables
:context-buffer context-buffer
:agent-name agent-name
:outgoing-request-decorator (when context-buffer
(map-elt (buffer-local-value 'agent-shell--state context-buffer)
:outgoing-request-decorator)))))
Expand Down Expand Up @@ -1518,7 +1527,8 @@ See also `agent-shell-confirm-interrupt'."
:client (map-elt (agent-shell--state) :client)
:notification (acp-make-session-cancel-notification
:session-id (map-nested-elt (agent-shell--state) '(:session :id))
:reason "User cancelled"))))
:reason "User cancelled")))
(call-interactively #'shell-maker-interrupt))
(t
(agent-shell--shutdown)
(call-interactively #'shell-maker-interrupt))))
Expand Down Expand Up @@ -1622,7 +1632,6 @@ Flow:
:response (agent-shell-viewport--response))))
(when (agent-shell--initialize-client)
(agent-shell--handle :command command :shell-buffer shell-buffer)))
;; Needs ACP subscriptions
((or (not (map-nested-elt (agent-shell--state) '(:client :request-handlers)))
(not (map-nested-elt (agent-shell--state) '(:client :notification-handlers)))
(not (map-nested-elt (agent-shell--state) '(:client :error-handlers))))
Expand Down Expand Up @@ -1813,10 +1822,26 @@ pretty-printed JSON inside a json fence."
(json-pretty-print-buffer)
(buffer-string)))))

(defun agent-shell--foreign-session-notification-p (state acp-notification)
"Return non-nil when ACP-NOTIFICATION belongs to a session other than STATE's.

The shared proxy connection is multiplexed across buffers and routes by
sessionId. This guards against a notification ever being rendered in a
buffer that does not own its session."
(when-let* ((notification-session (map-nested-elt acp-notification '(params sessionId)))
(own-session (map-nested-elt state '(:session :id))))
(not (equal notification-session own-session))))

(cl-defun agent-shell--on-notification (&key state acp-notification)
"Handle incoming ACP-NOTIFICATION using STATE."
(map-put! state :last-activity-time (current-time))
(cond ((and (not (agent-shell--active-requests-p state))
(cond ((agent-shell--foreign-session-notification-p state acp-notification)
;; Defensive: never render another session's output in this buffer.
(when acp-logging-enabled
(message "agent-shell: dropped notification for session %s (buffer owns %s)"
(map-nested-elt acp-notification '(params sessionId))
(map-nested-elt state '(:session :id)))))
((and (not (agent-shell--active-requests-p state))
(agent-shell--session-bound-notification-p acp-notification))
;; Turn-bound notification arriving with no agent request in
;; flight is a protocol violation: these notifications must
Expand Down Expand Up @@ -2053,17 +2078,24 @@ pretty-printed JSON inside a json fence."
:navigation 'never))
(agent-shell--cancel-idle-timer)
(agent-shell--emit-event
:event 'tool-call-update
:event 'tool-call-update
:data (list (cons :tool-call-id (map-nested-elt acp-notification '(params update toolCallId)))
(cons :tool-call (map-nested-elt state `(:tool-calls ,(map-nested-elt acp-notification '(params update toolCallId)))))))
(let* ((diff (map-nested-elt state `(:tool-calls ,(map-nested-elt acp-notification '(params update toolCallId)) :diff)))
;; Render from the persisted `:content' rather than this
;; notification's content. Some agents stream the output
;; in an earlier `tool_call_update' and then send the
;; "completed" update with only `rawOutput' (no
;; `content'); `agent-shell--save-tool-call' preserves the
;; streamed content (it drops nil overrides), so reading
;; from state keeps the output visible on completion.
(output (concat
"\n\n"
;; TODO: Consider if there are other
;; types of content to display.
(mapconcat (lambda (item)
(map-nested-elt item '(content text)))
(map-nested-elt acp-notification '(params update content))
(map-nested-elt state `(:tool-calls ,(map-nested-elt acp-notification '(params update toolCallId)) :content))
"\n\n")
"\n\n"))
(diff-text (agent-shell--format-diff-as-text diff))
Expand Down Expand Up @@ -2186,6 +2218,43 @@ pretty-printed JSON inside a json fence."
:append t
:above-last-prompt (not (shell-maker-busy)))
(map-put! state :last-entry-type nil))))
((equal (map-elt acp-notification 'method) "agent/ext-notification")
(let* ((orig-method (map-nested-elt acp-notification '(params method)))
(orig-params (map-nested-elt acp-notification '(params params))))
(cond
((and orig-method
(string-match-p "rate.limit\\|error/rate" orig-method))
(agent-shell--update-fragment
:state state
:block-id (format "%s-rate-limit" (map-elt state :request-count))
:body (or (map-elt orig-params 'message)
"Rate limit exceeded. Please wait a moment before trying again.")
:create-new t)
(map-put! state :last-entry-type nil))
(acp-logging-enabled
(message "Agent ext notification: %s" orig-method)))))
((equal (map-elt acp-notification 'method) "agent/disconnected")
(agent-shell-heartbeat-stop :heartbeat (map-elt state :heartbeat))
(let ((exit-code (map-nested-elt acp-notification '(params exitCode)))
(agent-name (map-nested-elt acp-notification '(params agentName))))
(agent-shell--update-fragment
:state state
:block-id (format "agent-disconnected-%s" (map-elt state :request-count))
:body (agent-shell--make-error-dialog-text
:code exit-code
:message (format "Agent '%s' disconnected (exit code: %s)"
(or agent-name "?")
(or exit-code "unknown")))
:create-new t)
(agent-shell--emit-event :event 'error
:data (list (cons :code exit-code)
(cons :message (format "Agent '%s' disconnected"
(or agent-name "?")))))
(shell-maker-finish-output :config shell-maker--config :success nil)))
((equal (map-elt acp-notification 'method) "agent/stderr")
(let ((agent-name (map-nested-elt acp-notification '(params agentName)))
(line (map-nested-elt acp-notification '(params line))))
(message "[%s] %s" (or agent-name "agent") line)))
(acp-logging-enabled
(agent-shell--update-fragment
:state state
Expand Down Expand Up @@ -4559,7 +4628,7 @@ through to `acp-send-request'."
(map-elt state :active-requests)))
(when on-success
(funcall on-success acp-response)))
:on-failure (lambda (acp-error raw-message)
:on-failure (lambda (acp-error &optional raw-message)
(map-put! state :active-requests
(seq-remove (lambda (r)
(equal r request))
Expand Down Expand Up @@ -5104,6 +5173,7 @@ Falls back to latest session in batch mode (e.g. tests)."
:session (agent-shell--session-from-response
:acp-response acp-response
:acp-session-id acp-session-id))
(acp-set-session-id (map-elt agent-shell--state :client) acp-session-id)
(agent-shell--save-config-options
:state agent-shell--state
:acp-config-options (map-elt acp-response 'configOptions)))
Expand Down Expand Up @@ -5203,6 +5273,8 @@ overwrites an existing fragment with equivalent content."
:session (agent-shell--session-from-response
:acp-response acp-response
:acp-session-id (map-elt acp-response 'sessionId)))
(acp-set-session-id (map-elt agent-shell--state :client)
(map-elt acp-response 'sessionId))
(agent-shell--save-config-options
:state agent-shell--state
:acp-config-options (map-elt acp-response 'configOptions))
Expand Down Expand Up @@ -5683,6 +5755,7 @@ Each entry is normalized via `agent-shell--make-mcp-server'."
"Subscribe SHELL and STATE to ACP events."
(acp-subscribe-to-errors
:client (map-elt state :client)
:buffer (map-elt state :buffer)
:on-error (lambda (acp-error)
(agent-shell--update-fragment
:state state
Expand All @@ -5696,10 +5769,12 @@ Each entry is normalized via `agent-shell--make-mcp-server'."
:above-last-prompt (not (shell-maker-busy)))))
(acp-subscribe-to-notifications
:client (map-elt state :client)
:buffer (map-elt state :buffer)
:on-notification (lambda (acp-notification)
(agent-shell--on-notification :state state :acp-notification acp-notification)))
(acp-subscribe-to-requests
:client (map-elt state :client)
:buffer (map-elt state :buffer)
:on-request (lambda (acp-request)
(agent-shell--on-request :state state :acp-request acp-request))))

Expand Down Expand Up @@ -6108,7 +6183,7 @@ Each marked span is replaced by its `agent-shell-region-text' value."
(agent-shell-viewport--update-header)))
(when success
(agent-shell--process-pending-request))))
:on-failure (lambda (acp-error raw-message)
:on-failure (lambda (acp-error &optional raw-message)
;; Display pending requests on failure.
(agent-shell--display-pending-requests)
(funcall (agent-shell--make-error-handler :state agent-shell--state :shell-buffer shell-buffer)
Expand Down
13 changes: 13 additions & 0 deletions src/agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Stored state for a connected ACP agent.

use agent_client_protocol::{ConnectionTo, role::acp::Agent};

/// A connected ACP agent and its negotiated metadata.
pub struct AgentConnection {
/// ACP connection handle. Clone-able; backed by internal channels.
pub connection: ConnectionTo<Agent>,
/// Serialized agent capabilities from the initialize response.
pub capabilities: serde_json::Value,
/// Serialized auth methods from the initialize response.
pub auth_methods: serde_json::Value,
}
Loading
Loading