Skip to content

Commit ea083cc

Browse files
committed
Add config/updated notification
1 parent 41017f2 commit ea083cc

File tree

12 files changed

+220
-65
lines changed

12 files changed

+220
-65
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- New server notification `config/updated` used to notify clients when a relevant config changed (behaviors, models etc).
6+
- Deprecate info inside `initialize` response, clients should use `config/updated` now.
7+
58
## 0.41.0
69

710
- Improve anthropic extraPayload requirement when adding models.

docs/protocol.md

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -149,33 +149,7 @@ type ChatBehavior = 'agent' | 'plan';
149149
_Response:_
150150

151151
```typescript
152-
interface InitializeResponse {
153-
154-
/*
155-
* The models supported by the server.
156-
*/
157-
models: ChatModel[];
158-
159-
/*
160-
* Default model used by server.
161-
*/
162-
chatDefaultModel: ChatModel;
163-
164-
/*
165-
* The chat behaviors available.
166-
*/
167-
chatBehaviors: ChatBehavior[];
168-
169-
/*
170-
* Default chat behavior used by server.
171-
*/
172-
chatDefaultBehavior: ChatBehavior;
173-
174-
/*
175-
* The chat welcome message when chat is cleared or in a new state.
176-
*/
177-
chatWelcomeMessage: string;
178-
}
152+
interface InitializeResponse {}
179153
```
180154

181155
### Initialized (➡️)
@@ -1044,6 +1018,36 @@ Soon
10441018

10451019
## Configuration
10461020

1021+
### Config updated (⬅️)
1022+
1023+
A server notification with the new config server is considering (models, behaviors etc), usually related to config or auth changes.
1024+
Clients should update UI accordingly, if a field is missing/null, means it has no change since last config used, so clients should ignore.
1025+
1026+
_Notification:_
1027+
1028+
* method: `config/updated`
1029+
* params: `configUpdatedParams` defined as follows:
1030+
1031+
```typescript
1032+
interface ConfigUpdatedParams {
1033+
/**
1034+
* Configs related to chat.
1035+
*/
1036+
chat?: {
1037+
1038+
/**
1039+
* The models the user can use in chat.
1040+
*/
1041+
models?: ChatModel[];
1042+
1043+
/**
1044+
* The chat behaviors the user can select.
1045+
*/
1046+
behaviors?: ChatBehavior[];
1047+
}
1048+
}
1049+
```
1050+
10471051
### Tool updated (⬅️)
10481052

10491053
A server notification about a tool status update like a MCP or native tool.

src/eca/config.clj

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
[cheshire.core :as json]
1111
[cheshire.factory :as json.factory]
1212
[clojure.core.memoize :as memoize]
13+
[clojure.data :as data]
1314
[clojure.java.io :as io]
1415
[clojure.string :as string]
1516
[eca.logger :as logger]
17+
[eca.messenger :as messenger]
1618
[eca.shared :as shared])
1719
(:import
1820
[java.io File]))
@@ -25,6 +27,8 @@
2527
(def ^:dynamic *global-config-error* false)
2628
(def ^:dynamic *local-config-error* false)
2729

30+
(def ^:private listen-idle-ms 3000)
31+
2832
(def initial-config
2933
{:providers {"openai" {:api "openai-responses"
3034
:url "https://api.openai.com"
@@ -210,3 +214,66 @@
210214

211215
;; all good
212216
:else nil))
217+
218+
(defn listen-for-changes! [db*]
219+
(while (not (:stopping @db*))
220+
(Thread/sleep ^long listen-idle-ms)
221+
(let [db @db*
222+
new-config (all db)
223+
new-config-hash (hash new-config)]
224+
(when (not= new-config-hash (:config-hash db))
225+
(swap! db* assoc :config-hash new-config-hash)
226+
(doseq [config-updated-fns (vals (:config-updated-fns db))]
227+
(config-updated-fns new-config))))))
228+
229+
(defn diff-keeping-vectors
230+
"Like (second (clojure.data/diff a b)) but if a value is a vector, keep vector value from b.
231+
232+
Example1: (diff-keeping-vectors {:a 1 :b 2} {:a 1 :b 3}) => {:b 3}
233+
Example2: (diff-keeping-vectors {:a 1 :b [:bar]} {:b [:bar :foo]}) => {:b [:bar :foo]}"
234+
[a b]
235+
(letfn [(diff-maps [a b]
236+
(let [all-keys (set (concat (keys a) (keys b)))]
237+
(reduce
238+
(fn [acc k]
239+
(let [a-val (get a k)
240+
b-val (get b k)]
241+
(cond
242+
;; Key doesn't exist in b, skip
243+
(and (contains? a k) (not (contains? b k)))
244+
acc
245+
246+
;; Key doesn't exist in a, include from b
247+
(and (not (contains? a k)) (contains? b k))
248+
(assoc acc k b-val)
249+
250+
;; Both are vectors and they differ, use the entire vector from b
251+
(and (vector? a-val) (vector? b-val) (not= a-val b-val))
252+
(assoc acc k b-val)
253+
254+
;; Both are maps, recurse
255+
(and (map? a-val) (map? b-val))
256+
(let [nested-diff (diff-maps a-val b-val)]
257+
(if (seq nested-diff)
258+
(assoc acc k nested-diff)
259+
acc))
260+
261+
;; Values are different, use value from b
262+
(not= a-val b-val)
263+
(assoc acc k b-val)
264+
265+
;; Values are the same, skip
266+
:else
267+
acc)))
268+
{}
269+
all-keys)))]
270+
(let [result (diff-maps a b)]
271+
(when (seq result)
272+
result))))
273+
274+
(defn notify-fields-changed-only! [config-updated messenger db*]
275+
(let [config-to-notify (diff-keeping-vectors (:last-config-notified @db*)
276+
config-updated)]
277+
(when (seq config-to-notify)
278+
(swap! db* update :last-config-notified shared/deep-merge config-to-notify)
279+
(messenger/config-updated messenger config-to-notify))))

src/eca/db.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
{:client-info {}
2121
:workspace-folders []
2222
:client-capabilities {}
23+
:config-hash nil
24+
:providers-config-hash nil
25+
:last-config-notified {}
26+
:stopping false
2327
:chat-behaviors ["agent" "plan"]
2428
:models {}
2529
:mcp-clients {}

src/eca/handlers.clj

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@
1515

1616
(set! *warn-on-reflection* true)
1717

18-
(defn ^:private initialize-models! [db* config]
18+
(defn ^:private sync-models! [db* config on-models-updated]
1919
(let [all-models (models/all)
20-
eca-models (reduce
20+
all-models (reduce
2121
(fn [p [provider provider-config]]
2222
(merge p
2323
(reduce
2424
(fn [m [model _model-config]]
2525
(let [full-model (str provider "/" model)
2626
model-capabilities (merge
2727
(or (get all-models full-model)
28-
;; we guess the capabilities from
29-
;; the first model with same name
28+
;; we guess the capabilities from
29+
;; the first model with same name
3030
(when-let [found-full-model (first (filter #(= (shared/normalize-model-name model)
3131
(shared/normalize-model-name (second (string/split % #"/" 2))))
3232
(keys all-models)))]
@@ -38,57 +38,84 @@
3838
{}
3939
(:models provider-config))))
4040
{}
41-
(:providers config))]
42-
(swap! db* update :models merge eca-models))
43-
(when-let [ollama-models (seq (llm-api/extra-models config))]
44-
(let [models (reduce
45-
(fn [models {:keys [model] :as ollama-model}]
46-
(assoc models
47-
(str config/ollama-model-prefix model)
48-
(select-keys ollama-model [:tools :reason?])))
49-
{}
50-
ollama-models)]
51-
(swap! db* update :models merge models))))
41+
(:providers config))
42+
all-models (if-let [local-models (seq (llm-api/local-models config))]
43+
(let [models (reduce
44+
(fn [models {:keys [model] :as ollama-model}]
45+
(assoc models
46+
(str config/ollama-model-prefix model)
47+
(select-keys ollama-model [:tools :reason?])))
48+
{}
49+
local-models)]
50+
(swap! db* update :models merge models))
51+
all-models)]
52+
(swap! db* assoc :models all-models)
53+
(on-models-updated)))
5254

53-
(defn initialize [{:keys [db* messenger]} params]
55+
(defn initialize [{:keys [db*]} params]
5456
(logger/logging-task
5557
:eca/initialize
5658
(reset! config/initialization-config* (shared/map->camel-cased-map (:initialization-options params)))
5759
(let [config (config/all @db*)]
60+
(logger/debug "Considered config: " config)
5861
(swap! db* assoc
5962
:client-info (:client-info params)
6063
:workspace-folders (:workspace-folders params)
6164
:client-capabilities (:capabilities params))
62-
(initialize-models! db* config)
6365
(when-not (:pureConfig config)
6466
(db/load-db-from-cache! db*))
65-
(future
66-
(Thread/sleep 1000) ;; wait chat window is open in some editors.
67-
(when-let [error (config/validation-error)]
68-
(messenger/chat-content-received
69-
messenger
70-
{:role "system"
71-
:content {:type :text
72-
:text (format "\nFailed to parse '%s' config, check stderr logs, double check your config and restart\n"
73-
error)}})))
74-
(logger/debug "Considered config: " config)
75-
{:models (sort (keys (:models @db*)))
76-
:chat-default-model (f.chat/default-model @db* config)
77-
:chat-behaviors (:chat-behaviors @db*)
78-
:chat-default-behavior (or (:defaultBehavior (:chat config)) ;;legacy
79-
(:defaultBehavior config))
80-
:chat-welcome-message (or (:welcomeMessage (:chat config)) ;;legacy
81-
(:welcomeMessage config))})))
67+
68+
;; Deprecated
69+
;; For backward compatibility,
70+
;; we now return chat config via `config/updated` notification.
71+
(sync-models! db* config (fn []))
72+
(let [db @db*]
73+
{:models (sort (keys (:models db)))
74+
:chat-default-model (f.chat/default-model db config)
75+
:chat-behaviors (:chat-behaviors db)
76+
:chat-default-behavior (or (:defaultBehavior (:chat config)) ;;legacy
77+
(:defaultBehavior config))
78+
:chat-welcome-message (or (:welcomeMessage (:chat config)) ;;legacy
79+
(:welcomeMessage config))}))))
8280

8381
(defn initialized [{:keys [db* messenger config]}]
82+
(let [sync-models-and-notify! (fn [config]
83+
(let [new-providers-hash (hash (:providers config))]
84+
(when (not= (:providers-config-hash @db*) new-providers-hash)
85+
(swap! db* assoc :providers-config-hash new-providers-hash)
86+
(sync-models! db* config (fn []
87+
(let [db @db*]
88+
(config/notify-fields-changed-only!
89+
{:chat
90+
{:models (sort (keys (:models db)))
91+
:default-model (f.chat/default-model db config)
92+
:behaviors (:chat-behaviors db)
93+
:default-behavior (or (:defaultBehavior (:chat config)) ;;legacy
94+
(:defaultBehavior config))
95+
:welcome-message (or (:welcomeMessage (:chat config)) ;;legacy
96+
(:welcomeMessage config))}}
97+
messenger
98+
db*)))))))]
99+
(swap! db* assoc-in [:config-updated-fns :sync-models] #(sync-models-and-notify! %))
100+
(sync-models-and-notify! config))
101+
(future
102+
(Thread/sleep 1000) ;; wait chat window is open in some editors.
103+
(when-let [error (config/validation-error)]
104+
(messenger/chat-content-received
105+
messenger
106+
{:role "system"
107+
:content {:type :text
108+
:text (format "\nFailed to parse '%s' config, check stderr logs, double check your config and restart\n"
109+
error)}}))
110+
(config/listen-for-changes! db*))
84111
(future
85112
(f.tools/init-servers! db* messenger config)))
86113

87114
(defn shutdown [{:keys [db*]}]
88115
(logger/logging-task
89116
:eca/shutdown
90117
(f.mcp/shutdown! db*)
91-
(reset! db* db/initial-db)
118+
(swap! db* assoc :stopping true)
92119
nil))
93120

94121
(defn chat-prompt [{:keys [messenger db* config]} params]

src/eca/llm_api.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
(or (get-in config [:providers (name provider) :url])
4242
(some-> (get-in config [:providers (name provider) :urlEnv]) config/get-env)))
4343

44-
(defn extra-models [config]
44+
(defn local-models [config]
4545
(let [ollama-api-url (provider-api-url "ollama" config)]
4646
(mapv
4747
(fn [{:keys [model] :as ollama-model}]

src/eca/messenger.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
(defprotocol IMessenger
88
(chat-content-received [this data])
99
(tool-server-updated [this params])
10+
(config-updated [this params])
1011
(showMessage [this msg])
1112
(editor-diagnostics [this uri]))

src/eca/server.clj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@
9898
(chat-content-received [_this content]
9999
(lsp.server/discarding-stdout
100100
(lsp.server/send-notification server "chat/contentReceived" content)))
101+
(config-updated [_this params]
102+
(lsp.server/discarding-stdout
103+
(lsp.server/send-notification server "config/updated" params)))
101104
(tool-server-updated [_this params]
102105
(lsp.server/discarding-stdout
103106
(lsp.server/send-notification server "tool/serverUpdated" params)))

src/eca/shared.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
(System/lineSeparator))
1919

2020
(defn uri->filename [uri]
21-
(let [uri (URI. uri)]
21+
(let [uri (-> (URI. uri)
22+
(.toASCIIString)
23+
(URI.))]
2224
(-> uri Paths/get .toString
2325
;; WINDOWS drive letters
2426
(string/replace #"^[a-z]:\\" string/upper-case))))

0 commit comments

Comments
 (0)