Skip to content

Commit 38c9f84

Browse files
committed
Support rollback chats
Fixes #42
1 parent 349a005 commit 38c9f84

File tree

11 files changed

+148
-39
lines changed

11 files changed

+148
-39
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
- Add support to rollback messages via `chat/rollback` and `chat/clear` messages. #42
6+
57
## 0.79.1
68

79
- Improve system prompt to add project env context.

docs/protocol.md

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,19 @@ type ChatContent =
555555
| ChatToolCalledContent
556556
| ChatToolCallRejectedContent
557557
| ChatMetadataContent;
558-
558+
559559
/**
560560
* Simple text message from the LLM
561561
*/
562-
interface ChatTextContent {
562+
interface ChatTextContent extends BaseChatContent {
563563
type: 'text';
564+
565+
/**
566+
* The unique identifier of this content.
567+
* Mostly used to rollback messages.
568+
*/
569+
contentId: string;
570+
564571
/**
565572
* The text content
566573
*/
@@ -1224,6 +1231,62 @@ interface ChatPromptStopParams {
12241231
}
12251232
```
12261233

1234+
### Chat rollback (↩️)
1235+
1236+
A client request to rollback chat messages to before a specific user sent message using `contentId`.
1237+
Clients should show an option close to user sent messages in chat to rollback, calling this method.
1238+
Server will then remove the messages from its memory after that contentId and produce `chat/cleared` followed
1239+
with `chat/contentReceived` with the kept messages.
1240+
1241+
_Request:_
1242+
1243+
* method: `chat/rollback`
1244+
* params: `ChatRollbackParams` defined as follows:
1245+
1246+
```typescript
1247+
interface ChatRollbackParams {
1248+
/**
1249+
* The chat session identifier.
1250+
*/
1251+
chatId: string;
1252+
1253+
/**
1254+
* The message content id.
1255+
*/
1256+
contentId: string;
1257+
}
1258+
```
1259+
1260+
_Response:_
1261+
1262+
```typescript
1263+
interface ChatRollbackResponse {}
1264+
```
1265+
1266+
### Chat cleared (⬅️)
1267+
1268+
A server notification to clear a chat UI, currently supporting removing only messages of the chat.
1269+
1270+
_Request:_
1271+
1272+
* method: `chat/cleared`
1273+
* params: `ChatClearedParams` defined as follows:
1274+
1275+
```typescript
1276+
interface ChatClearedParams {
1277+
1278+
/**
1279+
* The chat session identifier.
1280+
*/
1281+
chatId: string;
1282+
1283+
/**
1284+
* Whether to clear the messages of a chat.
1285+
*/
1286+
messages: boolean;
1287+
}
1288+
```
1289+
12271290
### Chat delete (↩️)
12281291

12291292
A client request to delete a existing chat, removing all previous messages and used tokens/costs from memory, good for reduce context or start a new clean chat.
@@ -1243,6 +1306,12 @@ interface ChatDeleteParams {
12431306
}
12441307
```
12451308

1309+
_Response:_
1310+
1311+
```typescript
1312+
interface ChatDeleteResponse {}
1313+
```
1314+
12461315
### Chat selected behavior changed (➡️)
12471316

12481317
A client notification for server telling the user selected a different behavior in chat.
@@ -1261,12 +1330,6 @@ interface ChatSelectedBehaviorChanged {
12611330
}
12621331
```
12631332

1264-
_Response:_
1265-
1266-
```typescript
1267-
interface ChatDeleteResponse {}
1268-
```
1269-
12701333
### Editor diagnostics (↪️)
12711334

12721335
A server request to retrieve LSP or any other kind of diagnostics if available from current workspaces.

src/eca/features/chat.clj

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
(def ^:private logger-tag "[CHAT]")
2626

27+
(defn ^:private new-content-id []
28+
(str (random-uuid)))
29+
2730
(defn default-model [db config]
2831
(llm-api/default-model db config))
2932

@@ -600,7 +603,7 @@
600603
:on-first-response-received (fn [& _]
601604
(assert-chat-not-stopped! chat-ctx)
602605
(doseq [message user-messages]
603-
(add-to-history! message))
606+
(add-to-history! (assoc message :content-id (:user-content-id chat-ctx))))
604607
(send-content! chat-ctx :system {:type :progress
605608
:state :running
606609
:text "Generating"}))
@@ -622,7 +625,8 @@
622625

623626
(finish-chat-prompt! :idle chat-ctx))
624627
:finish (do
625-
(add-to-history! {:role "assistant" :content [{:type :text :text @received-msgs*}]})
628+
(add-to-history! {:role "assistant"
629+
:content [{:type :text :text @received-msgs*}]})
626630
(finish-chat-prompt! :idle chat-ctx))))
627631
:on-prepare-tool-call (fn [{:keys [id full-name arguments-text]}]
628632
(assert-chat-not-stopped! chat-ctx)
@@ -919,24 +923,29 @@
919923
:id (:id message-content)
920924
:total-time-ms (:total-time-ms message-content)}]))
921925

926+
(defn ^:private send-chat-contents! [messages chat-ctx]
927+
(doseq [message messages]
928+
(doseq [chat-content (message-content->chat-content (:role message) (:content message))]
929+
(send-content! chat-ctx
930+
(:role message)
931+
chat-content))))
932+
922933
(defn ^:private handle-command! [{:keys [command args]} chat-ctx]
923934
(let [{:keys [type on-finished-side-effect] :as result} (f.commands/handle-command! command args chat-ctx)]
924935
(case type
925936
:chat-messages (do
926937
(doseq [[chat-id {:keys [messages title]}] (:chats result)]
927-
(doseq [message messages]
928-
(let [new-chat-ctx (assoc chat-ctx :chat-id chat-id)]
929-
(doseq [chat-content (message-content->chat-content (:role message) (:content message))]
930-
(send-content! new-chat-ctx
931-
(:role message)
932-
chat-content))
933-
(when title
934-
(send-content! new-chat-ctx :system (assoc-some
935-
{:type :metadata}
936-
:title title))))))
938+
(let [new-chat-ctx (assoc chat-ctx :chat-id chat-id)]
939+
(send-chat-contents! messages new-chat-ctx)
940+
(when title
941+
(send-content! new-chat-ctx :system (assoc-some
942+
{:type :metadata}
943+
:title title)))))
937944
(finish-chat-prompt! :idle chat-ctx))
938945
:new-chat-status (finish-chat-prompt! (:status result) chat-ctx)
939-
:send-prompt (prompt-messages! [{:role "user" :content (:prompt result)}] (assoc chat-ctx :on-finished-side-effect on-finished-side-effect))
946+
:send-prompt (prompt-messages! [{:role "user"
947+
:content (:prompt result)}]
948+
(assoc chat-ctx :on-finished-side-effect on-finished-side-effect))
940949
nil)))
941950

942951
(defn prompt
@@ -995,6 +1004,7 @@
9951004
:db* db*
9961005
:metrics metrics
9971006
:config config
1007+
:user-content-id (new-content-id)
9981008
:messenger messenger}
9991009
decision (message->decision message db config)
10001010
hook-outputs* (atom [])
@@ -1014,6 +1024,7 @@
10141024
user-messages)]
10151025
(swap! db* assoc-in [:chats chat-id :status] :running)
10161026
(send-content! chat-ctx :user {:type :text
1027+
:content-id (:user-content-id chat-ctx)
10171028
:text (str message "\n")})
10181029
(case (:type decision)
10191030
:mcp-prompt (send-mcp-prompt! decision chat-ctx)
@@ -1098,3 +1109,20 @@
10981109
[{:keys [chat-id]} db* metrics]
10991110
(swap! db* update :chats dissoc chat-id)
11001111
(db/update-workspaces-cache! @db* metrics))
1112+
1113+
(defn rollback-chat
1114+
"Remove messages from chat in db until content-id matches.
1115+
Then notify to clear chat and then the kept messages."
1116+
[{:keys [chat-id content-id]} db* messenger]
1117+
(let [all-messages (get-in @db* [:chats chat-id :messages])
1118+
new-messages (vec (take-while #(not= (:content-id %) content-id) all-messages))]
1119+
(swap! db* assoc-in [:chats chat-id :messages] new-messages)
1120+
(messenger/chat-cleared
1121+
messenger
1122+
{:chat-id chat-id
1123+
:messages true})
1124+
(send-chat-contents!
1125+
new-messages
1126+
{:chat-id chat-id
1127+
:messenger messenger})
1128+
{}))

src/eca/features/prompt.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"<contexts description=\"User-Provided. This content is current and accurate. Treat this as sufficient context for answering the query.\">"
6565
""
6666
(reduce
67-
(fn [context-str {:keys [type path position content lines-range uri] :as a}]
67+
(fn [context-str {:keys [type path position content lines-range uri]}]
6868
(str context-str (case type
6969
:file (if lines-range
7070
(format "<file line-start=%s line-end=%s path=\"%s\">%s</file>\n\n"

src/eca/handlers.clj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@
116116
(f.chat/delete-chat params db* metrics)
117117
{}))
118118

119+
(defn chat-rollback [{:keys [db* metrics messenger]} params]
120+
(metrics/task metrics :eca/chat-rollback
121+
(f.chat/rollback-chat params db* messenger)))
122+
119123
(defn mcp-stop-server [{:keys [db* messenger metrics config]} params]
120124
(metrics/task metrics :eca/mcp-stop-server
121125
(f.tools/stop-server! (:name params) db* messenger config metrics)))
@@ -165,4 +169,4 @@
165169
[{:keys [db* config metrics messenger]} params]
166170
(metrics/task metrics :eca/rewrite-prompt
167171
(handle-expected-errors
168-
(f.rewrite/prompt params db* config messenger metrics))))
172+
(f.rewrite/prompt params db* config messenger metrics))))

src/eca/llm_providers/anthropic.clj

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,24 +131,26 @@
131131
:content [{:type "thinking"
132132
:signature (:external-id content)
133133
:thinking (:text content)}]}
134-
(update msg :content (fn [c]
134+
(-> msg
135+
(dissoc :content-id)
136+
(update :content (fn [c]
135137
(if (string? c)
136138
(string/trim c)
137139
(vec
138-
(keep #(case (name (:type %))
140+
(keep #(case (name (:type %))
139141

140-
"text"
141-
(update % :text string/trim)
142+
"text"
143+
(update % :text string/trim)
142144

143-
"image"
144-
(when supports-image?
145-
{:type "image"
146-
:source {:data (:base64 %)
147-
:media_type (:media-type %)
148-
:type "base64"}})
145+
"image"
146+
(when supports-image?
147+
{:type "image"
148+
:source {:data (:base64 %)
149+
:media_type (:media-type %)
150+
:type "base64"}})
149151

150-
%)
151-
c)))))))
152+
%)
153+
c))))))))
152154
past-messages))
153155

154156
(defn ^:private add-cache-to-last-message [messages]

src/eca/llm_providers/ollama.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
(assoc :name (:full-name tool)))})
9696
tools))
9797

98-
(defn ^:private normalize-messages [past-messages]
98+
(defn ^:private normalize-messages [messages]
9999
(mapv (fn [{:keys [role content] :as msg}]
100100
(case role
101101
"tool_call" {:role "assistant" :tool-calls [{:type "function"
@@ -111,7 +111,7 @@
111111
;; TODO add image supprt
112112
;; :images []
113113
}))
114-
past-messages))
114+
messages))
115115

116116
(defn chat! [{:keys [model user-messages reason? instructions api-url past-messages tools]}
117117
{:keys [on-message-received on-error on-prepare-tool-call on-tools-called

src/eca/llm_providers/openai.clj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@
9494
[{:type "summary_text"
9595
:text (:text content)}])
9696
:encrypted_content (:external-id content)}
97-
(update msg :content (fn [c]
97+
(-> msg
98+
(dissoc :content-id)
99+
(update :content (fn [c]
98100
(if (string? c)
99101
c
100102
(keep #(case (name (:type %))
@@ -112,7 +114,7 @@
112114
(:base64 %))})
113115

114116
%)
115-
c))))))
117+
c)))))))
116118
messages))
117119

118120
(defn ^:private ->tools [tools]

src/eca/messenger.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
(defprotocol IMessenger
88
(chat-content-received [this data])
9+
(chat-cleared [this params])
910
(rewrite-content-received [this data])
1011
(tool-server-updated [this params])
1112
(config-updated [this params])

src/eca/server.clj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
(defmethod jsonrpc.server/receive-request "chat/delete" [_ components params]
6969
(handlers/chat-delete (with-config components) params))
7070

71+
(defmethod jsonrpc.server/receive-request "chat/rollback" [_ components params]
72+
(handlers/chat-rollback (with-config components) params))
73+
7174
(defmethod jsonrpc.server/receive-notification "mcp/stopServer" [_ components params]
7275
(handlers/mcp-stop-server (with-config components) params))
7376

@@ -112,6 +115,9 @@
112115
(chat-content-received [_this content]
113116
(jsonrpc.server/discarding-stdout
114117
(jsonrpc.server/send-notification server "chat/contentReceived" content)))
118+
(chat-cleared [_this params]
119+
(jsonrpc.server/discarding-stdout
120+
(jsonrpc.server/send-notification server "chat/cleared" params)))
115121
(rewrite-content-received [_this content]
116122
(jsonrpc.server/discarding-stdout
117123
(jsonrpc.server/send-notification server "rewrite/contentReceived" content)))

0 commit comments

Comments
 (0)