-
-
Notifications
You must be signed in to change notification settings - Fork 229
Expand file tree
/
Copy pathagent-shell-config.el
More file actions
330 lines (259 loc) · 12.9 KB
/
Copy pathagent-shell-config.el
File metadata and controls
330 lines (259 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
;;; agent-shell-config.el --- Session config option helpers for agent-shell. -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Alvaro Ramirez
;; Author: Alvaro Ramirez https://xenodium.com
;; URL: https://github.com/xenodium/agent-shell
;; This package is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This package is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Normalization, querying, and conversion of ACP session config options.
;;
;; ACP agents may advertise config options (model, mode, or custom) via
;; `configOptions' in session responses. This file converts the camelCase
;; ACP wire format into :kebab-case internal alists and provides accessors
;; for the rest of agent-shell.
;;
;; See https://agentclientprotocol.com/protocol/session-config-options
;;
;; Report issues at https://github.com/xenodium/agent-shell/issues
;;; Code:
(require 'cl-lib)
(require 'map)
(require 'seq)
(require 'agent-shell-faces)
;;; Normalization
(defun agent-shell--normalize-config-option (acp-option)
"Normalize ACP-OPTION (an ACP config option) to an internal alist.
For example:
(agent-shell--normalize-config-option
\\='((id . \"mode\") (type . \"select\") (currentValue . \"ask\")))
=> \\='((:id . \"mode\") (:type . \"select\") (:current-value . \"ask\") ...)"
`((:id . ,(map-elt acp-option 'id))
(:name . ,(map-elt acp-option 'name))
(:description . ,(map-elt acp-option 'description))
(:category . ,(map-elt acp-option 'category))
(:type . ,(map-elt acp-option 'type))
(:current-value . ,(map-elt acp-option 'currentValue))
(:options . ,(mapcar (lambda (acp-value)
`((:value . ,(map-elt acp-value 'value))
(:name . ,(map-elt acp-value 'name))
(:description . ,(map-elt acp-value 'description))))
(append (map-elt acp-option 'options) nil)))))
(defun agent-shell--normalize-config-options (acp-config-options)
"Normalize ACP-CONFIG-OPTIONS (ACP `configOptions') to internal alists.
For example:
(agent-shell--normalize-config-options
\\='[((id . \"mode\") (type . \"select\") (currentValue . \"ask\"))])
=> \\='(((:id . \"mode\") (:type . \"select\") (:current-value . \"ask\") ...))"
(mapcar #'agent-shell--normalize-config-option
(append acp-config-options nil)))
;;; State management
(cl-defun agent-shell--save-config-options (&key state acp-config-options)
"Save ACP-CONFIG-OPTIONS in STATE as normalized session config state.
Stores normalized options at both top-level :config-options and inside
the :session alist for consistency."
(let ((normalized-options (agent-shell--normalize-config-options acp-config-options)))
(setf (map-elt state :config-options) normalized-options)
(when-let* ((session (map-elt state :session)))
(setf (map-elt session :config-options) normalized-options)
(setf (map-elt state :session) session))))
(cl-defun agent-shell--config-option-set-value (&key state config-id value)
"In STATE, set the :current-value of config option CONFIG-ID to VALUE.
Updates the option in place in both top-level :config-options and the
copy under :session. Used to keep local state in sync when an agent
acknowledges a `session/set_config_option' without echoing the full
configOptions list."
(dolist (option (agent-shell--config-options state))
(when (equal config-id (map-elt option :id))
(setf (map-elt option :current-value) value))))
;;; Accessors
(defun agent-shell--config-options (state)
"Return current config options from STATE.
For example:
(agent-shell--config-options
\\='((:session . ((:config-options . (((:id . \"model\")))))))
=> \\='(((:id . \"model\")))"
(or (map-nested-elt state '(:session :config-options))
(map-elt state :config-options)))
(cl-defun agent-shell--config-option-get (&key state id)
"Return config option with ID from STATE, or nil.
For example:
(agent-shell--config-option-get :state state :id \"model\")
=> \\='((:id . \"model\") (:type . \"select\") ...)"
(seq-find (lambda (option)
(equal id (map-elt option :id)))
(agent-shell--config-options state)))
(defun agent-shell--config-option-by-category (state category)
"Return a config option in STATE matching CATEGORY, or nil.
CATEGORY may be nil for uncategorized options. Uses `equal' for
nil-safe comparison.
When several options share CATEGORY, prefers the one whose `:id'
equals CATEGORY, falling back to the first match. Some agents (e.g.
Cline) tag multiple options with the same category -- both their
`provider' and `model' options use category \"model\" -- so matching
on category alone could otherwise resolve \"model\" to the provider
option.
For example:
(agent-shell--config-option-by-category state \"model\")
=> \\='((:id . \"model\") (:category . \"model\") ...)"
(let ((matches (seq-filter (lambda (option)
(equal category (map-elt option :category)))
(agent-shell--config-options state))))
(or (seq-find (lambda (option)
(equal category (map-elt option :id)))
matches)
(car matches))))
(defun agent-shell--resolve-config-option (state option)
"Return the config option in STATE addressed by OPTION, or nil.
OPTION is matched against advertised ids first, then ACP categories, so
both what a shell lists under \"Available config options\" (\"effort\")
and the spec's category names (\"thought_level\") reach the same option.
Ids cast the wider net: an option outside the spec's categories, say
\"fast\", is only addressable by id.
For example, against an agent advertising an \"effort\" option
categorized as \"thought_level\":
(agent-shell--resolve-config-option state \"effort\")
=> \\='((:id . \"effort\") (:category . \"thought_level\") ...)
(agent-shell--resolve-config-option state \"thought_level\")
=> \\='((:id . \"effort\") (:category . \"thought_level\") ...)"
(or (agent-shell--config-option-get :state state :id option)
(agent-shell--config-option-by-category state option)))
(defun agent-shell--select-config-options (state)
"Return selectable (type = \"select\") config options from STATE."
(seq-filter (lambda (option)
(equal (map-elt option :type) "select"))
(agent-shell--config-options state)))
(defun agent-shell--config-option-value-name (option value)
"Return display name for VALUE in OPTION, falling back to VALUE itself.
For example:
(agent-shell--config-option-value-name
\\='((:options . (((:value . \"ask\") (:name . \"Ask\"))))) \"ask\")
=> \"Ask\""
(or (map-elt (seq-find (lambda (candidate)
(equal value (map-elt candidate :value)))
(map-elt option :options))
:name)
value))
;;; Legacy shape conversion
(defun agent-shell--config-option-as-models (option)
"Convert OPTION values to legacy model display shape.
Each value becomes an alist with :model-id, :name, and :description
so existing model UI code works unchanged.
For example:
(agent-shell--config-option-as-models
\\='((:options . (((:value . \"sonnet\")
(:name . \"Sonnet\")
(:description . nil))))))
=> \\='(((:model-id . \"sonnet\")
(:name . \"Sonnet\")
(:description . nil)))"
(mapcar (lambda (value)
`((:model-id . ,(map-elt value :value))
(:name . ,(map-elt value :name))
(:description . ,(map-elt value :description))))
(map-elt option :options)))
(defun agent-shell--config-option-as-modes (option)
"Convert OPTION values to legacy mode display shape.
Each value becomes an alist with :id, :name, and :description
so existing mode UI code works unchanged.
For example:
(agent-shell--config-option-as-modes
\\='((:options . (((:value . \"ask\") (:name . \"Ask\") (:description . nil))))))
=> \\='(((:id . \"ask\") (:name . \"Ask\") (:description . nil)))"
(mapcar (lambda (value)
`((:id . ,(map-elt value :value))
(:name . ,(map-elt value :name))
(:description . ,(map-elt value :description))))
(map-elt option :options)))
;;; Current value helpers
(defun agent-shell--current-model-id (state)
"Return current model ID from STATE.
Prefers config option with category \"model\", falls back to
session :model-id."
(or (map-elt (agent-shell--config-option-by-category state "model") :current-value)
(map-nested-elt state '(:session :model-id))))
(defun agent-shell--current-mode-id (state)
"Return current mode ID from STATE.
Prefers config option with category \"mode\", falls back to
session :mode-id."
(or (map-elt (agent-shell--config-option-by-category state "mode") :current-value)
(map-nested-elt state '(:session :mode-id))))
(defun agent-shell--current-thought-level-id (state)
"Return current thought level ID from STATE or nil if not advertised.
The option is identified by ACP category \"thought_level\" per the spec:
https://agentclientprotocol.com/protocol/session-config-options"
(map-elt (agent-shell--config-option-by-category state "thought_level")
:current-value))
(defun agent-shell--get-available-models (state)
"Return available models from STATE, preferring config options.
When a config option with category \"model\" exists, converts its
values to legacy model shape. Otherwise returns session :models."
(if-let* ((model-option (agent-shell--config-option-by-category state "model")))
(agent-shell--config-option-as-models model-option)
(map-nested-elt state '(:session :models))))
(defun agent-shell--get-available-thought-levels (state)
"Return available thought level values from STATE.
Each value is an alist with :value, :name, optional :description where
:value is the agent value and :name is a human-readable name."
(map-elt (agent-shell--config-option-by-category state "thought_level")
:options))
;;; Formatting
(defun agent-shell--format-available-config-options (config-options)
"Format CONFIG-OPTIONS for shell rendering.
Returns a propertized string with one block per option showing the
name (with any description inline), the current value, and the
selectable values. Both the current value and each selectable value
show the id sent to the agent alongside its display name.
The name carries the listing's emphasis; the `current:' and `values:'
labels are muted so they do not read as headings."
(string-join
(seq-map
(lambda (option)
(let* ((values-prefix "values: ")
(name (concat
(propertize (format "%s (id: %s)"
(map-elt option :name)
(map-elt option :id))
'font-lock-face 'agent-shell-list-name)
(when (map-elt option :description)
(concat ": " (map-elt option :description)))))
(current (concat
(propertize "current: " 'font-lock-face 'agent-shell-secondary)
(agent-shell--config-option-value-label
(agent-shell--config-option-value-name
option (map-elt option :current-value))
(map-elt option :current-value))))
(values (when-let* ((options (map-elt option :options)))
(concat
(propertize values-prefix 'font-lock-face 'agent-shell-secondary)
(string-join
(seq-map (lambda (value)
(agent-shell--config-option-value-label
(map-elt value :name)
(map-elt value :value)))
options)
(concat "\n" (make-string (length values-prefix) ?\s)))))))
(string-join (delq nil (list name current values)) "\n")))
config-options)
"\n\n"))
(defun agent-shell--config-option-value-label (name id)
"Return a display label combining NAME with its raw ID.
Shows the human NAME annotated with the ID sent to the agent, as in
\"High (id: high)\". Falls back to the bare ID when NAME is nil or
adds no information.
For example:
(agent-shell--config-option-value-label \"High\" \"high\")
=> \"High (id: high)\""
(if (and name (not (equal name id)))
(format "%s (id: %s)" name id)
(format "%s" id)))
(provide 'agent-shell-config)
;;; agent-shell-config.el ends here