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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Add message pagination over a shared cursor core: new JSON-RPC `chat/history` method and optional `limit`/`before`/`after` window params on `chat/open`, plus opt-in pagination on remote HTTP `GET /api/v1/chats/:id`. Opaque cursors with a `lastCompaction` sentinel; meta exposes before/after/compaction cursors.
- Support declaring image input for custom models via a per-model `imageInput` config flag; openai-chat now round-trips tool-result images. (#503)
- Bedrock: honor Anthropic thinking variants (effort + adaptive thinking) for Claude models, fixing Claude 4.7+ failing with `thinking.type.enabled` not supported. (#502)
- Reworked system-prompt handling: fix stale static cache, quote-safe context attributes, dedicated workspace-roots/MCP sections, content-named context headings (`## Context`/`## MCP Resources`), and a read-only, reorganized `/prompt-show`.

## 0.140.1

Expand Down
8 changes: 5 additions & 3 deletions integration-test/integration/chat/commands_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@
resp))

(match-content chat-id "user" {:type "text" :text "/prompt-show\n"})
(match-content chat-id "system" {:type "text" :text (m/pred #(and (string/includes? % "You are ECA")
(not (string/includes? % ":static"))
(not (string/includes? % ":dynamic"))))})
(match-content chat-id "system" {:type "text" :text (m/pred #(and (string/includes? % "# Instructions (System prompt)")
(string/includes? % "You are ECA")
(not (string/includes? % "# Chat (User prompt)"))
(not (string/includes? % "/prompt-show"))
(string/includes? % "Tool schemas are sent separately")))})
(match-content chat-id "system" {:type "progress" :state "finished"}))))

(deftest mcp-prompts
Expand Down
6 changes: 0 additions & 6 deletions resources/prompts/additional_system_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,3 @@ OS: {{osName}}
Default shell: {{shell}}
User: {{userName}}
Home directory: {{homeDir}}
Workspaces: {{workspaceRoots}}

**Path Resolution & Context:**
*Workspaces:* Directories containing code or data relevant to this session.
*Rule:* Use Workspaces as the base to resolve relative paths into absolute paths when required by tools.

3 changes: 1 addition & 2 deletions resources/prompts/code_agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ For each file, give a short description of what needs to be edited, then use the
{% if toolEnabled_eca__editor_diagnostics %}
After finishing your changes, use eca__editor_diagnostics to check for diagnostics, making sure you didn't introduce any errors or warnings.
{% endif %}

## Communication

The chat is markdown mode.
Expand All @@ -27,9 +26,9 @@ You have tools at your disposal to solve the coding task. Follow these rules reg
2. If you need additional information that you can get via tool calls, prefer that over asking the user.
3. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer.
4. You have the capability to call multiple tools in a single response, batch your tool calls together for optimal performance.

{% if toolEnabled_eca__task %}
## Task Tracking

You have access to the `eca__task` tool for task management.

Use `eca__task` as the canonical task list when you need to plan and track non-trivial, multi-step execution (e.g., multiple tasks, dependencies, or iterative debugging), or when the user explicitly asks for a plan/todo list. Skip it for a single small action or purely informational replies.
Expand Down
6 changes: 3 additions & 3 deletions src/eca/cache.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[babashka.fs :as fs]
[clojure.java.io :as io]
[clojure.string :as string]
[eca.digest :as digest]
[eca.logger :as logger])
(:import
[java.io File]))
Expand Down Expand Up @@ -53,11 +54,10 @@
Order-independent: the same set of folders always yields the same hash."
[workspaces uri->filename-fn]
(let [joined (string/join ":" (sorted-workspace-paths workspaces uri->filename-fn))
md (java.security.MessageDigest/getInstance "SHA-256")
digest (.digest (doto md (.update (.getBytes joined "UTF-8"))))
digest-bytes (digest/sha-256-bytes joined)
encoder (-> (java.util.Base64/getUrlEncoder)
(.withoutPadding))
key (.encodeToString encoder digest)]
key (.encodeToString encoder digest-bytes)]
(subs key 0 (min 8 (count key)))))

(def ^:private logger-tag "[CACHE]")
Expand Down
15 changes: 15 additions & 0 deletions src/eca/digest.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
(ns eca.digest
(:import
[java.nio.charset StandardCharsets]
[java.security MessageDigest]))

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

(defn sha-256-bytes ^bytes [^String s]
(-> (MessageDigest/getInstance "SHA-256")
(.digest (.getBytes s StandardCharsets/UTF_8))))

(defn sha-256-hex ^String [^String s]
(->> (sha-256-bytes s)
(map #(format "%02x" (bit-and % 0xff)))
(apply str)))
49 changes: 42 additions & 7 deletions src/eca/features/chat.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
[clojure.java.io :as io]
[clojure.set :as set]
[clojure.string :as string]
[eca.cache :as cache]
[eca.config :as config]
[eca.db :as db]
[eca.digest :as digest]
[eca.features.background-tasks :as bg]
[eca.features.chat.history :as history]
[eca.features.chat.lifecycle :as lifecycle]
Expand Down Expand Up @@ -64,6 +66,29 @@
(str (System/getProperty "user.name") "@ECA"
(when (not-empty agent) (str "/" agent))))

(defn ^:private static-prompt-cache-signature
"SHA-256 cache identity for static prompt reuse."
[refined-contexts static-rules path-scoped-rules skills agent config chat-id all-tools db]
(let [static-contexts (vec (filter f.prompt/static-prompt-context? refined-contexts))]
(digest/sha-256-hex
(pr-str
{:agent agent
:chat-prompt-template (f.prompt/eca-chat-prompt agent config chat-id db)
:workspace-roots (mapv (comp shared/uri->filename :uri) (:workspace-folders db))
:environment {:os-name (str (System/getProperty "os.name") " " (System/getProperty "os.version"))
:shell (or (System/getenv "SHELL") (System/getenv "ComSpec"))
:user-name (System/getProperty "user.name")
:home-dir (cache/user-home)}
:is-subagent (boolean (get-in db [:chats chat-id :subagent]))
:startup-context (get-in db [:chats chat-id :startup-context])
:static-contexts static-contexts
:repo-map (when (some #(= :repoMap (:type %)) static-contexts)
:present)
:static-rules (mapv #(select-keys % [:id :name :scope :content]) static-rules)
:path-scoped-rules (mapv #(select-keys % [:id :name :scope :workspace-root :paths :enforce]) path-scoped-rules)
:skills (mapv #(select-keys % [:name :description]) skills)
:tools (sort (map :full-name all-tools))}))))

(defn ^:private prune-tool-results!
"Prunes old tool result content from chat history to reduce context size.
Walks messages backwards, protecting the most recent tool outputs up to
Expand Down Expand Up @@ -1451,16 +1476,21 @@
agent)))))
repo-map* (delay (f.index/repo-map db config {:as-string? true}))
prompt-cache (get-in db [:chats chat-id :prompt-cache])
static-signature (static-prompt-cache-signature
refined-contexts static-rules path-scoped-rules skills
agent config chat-id all-tools db)
instructions (if (and prompt-cache
(= (:agent prompt-cache) agent)
(= (:model prompt-cache) full-model))
(= (:model prompt-cache) full-model)
(= (:static-signature prompt-cache) static-signature))
{:static (:static prompt-cache)
:dynamic (f.prompt/build-dynamic-instructions refined-contexts db)}
(let [result (f.prompt/build-chat-instructions
refined-contexts static-rules path-scoped-rules skills repo-map*
agent config chat-id all-tools db)]
(swap! db* assoc-in [:chats chat-id :prompt-cache]
{:static (:static result)
:static-signature static-signature
:agent agent
:model full-model})
result))
Expand All @@ -1470,15 +1500,21 @@
seq
(f.prompt/contexts-str repo-map* nil))]
[{:type :text :text contexts-str}])
decision (message->decision message db config)
;; Cursor (and other volatile editor-state) is delivered per-turn in the
;; user message - never the system prompt - and only re-sent when it
;; changed. This keeps the cached system/prefix stable across turns,
;; avoiding llama.cpp full prompt re-processing on every cursor move. #464
;; Only normal prompts send this synthesized user message to the model;
;; /prompt-show and MCP prompts use different messages.
editor-state-context (f.prompt/build-editor-state-context refined-contexts)
editor-state-contents (when (and editor-state-context
(not= editor-state-context
(get-in db [:chats chat-id :last-editor-state])))
(swap! db* assoc-in [:chats chat-id :last-editor-state] editor-state-context)
editor-state-changed? (and editor-state-context
(not= editor-state-context
(get-in db [:chats chat-id :last-editor-state])))
_ (when (and editor-state-changed?
(= :prompt-message (:type decision)))
(swap! db* assoc-in [:chats chat-id :last-editor-state] editor-state-context))
editor-state-contents (when editor-state-changed?
[{:type :text :text editor-state-context}])
user-messages [{:role "user" :content (vec (concat [{:type :text :text message}]
expanded-prompt-contexts
Expand All @@ -1492,8 +1528,7 @@
:full-model full-model
:provider provider
:model model
:messenger messenger})
decision (message->decision message db config)]
:messenger messenger})]
;; Show original prompt to user, but LLM receives the modified version
(lifecycle/send-content! chat-ctx :user {:type :text
:content-id (:user-content-id chat-ctx)
Expand Down
58 changes: 43 additions & 15 deletions src/eca/features/commands.clj
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,45 @@
(multi-str "Context Usage" "" header "" "Estimated usage" "" zipped))
(multi-str "Context Usage" header "" (mapv #(fmt-cat "" %) all-rows)))))

(defn ^:private prompt-show-section [title body]
(when-not (string/blank? body)
(multi-str
"────────────────────────────────────────"
(str "# " title)
"────────────────────────────────────────"
(string/trim body))))

(defn ^:private prompt-show-user-messages-body [user-messages]
(->> (mapcat :content user-messages)
(map-indexed (fn [idx content]
(when-let [text (shared/not-blank
(cond
(some? (:text content)) (:text content)
(= :image (:type content)) (format "[image: media-type=%s, base64 omitted (%s chars)]"
(:media-type content)
(count (or (:base64 content) "")))
:else (pr-str (dissoc content :base64))))]
(if (and (zero? idx) text)
(string/replace-first text #"^/prompt-show(?:\s+|$)" "")
text))))
(remove string/blank?)
(string/join "\n\n")))

(defn ^:private prompt-show-text [instructions user-messages]
(let [{:keys [static dynamic]} (if (map? instructions)
instructions
{:static instructions :dynamic nil})
system-prompt (->> [static dynamic]
(remove string/blank?)
(string/join "\n\n"))
sections (remove nil?
[(prompt-show-section "Instructions (System prompt)" system-prompt)
(prompt-show-section "Chat (User prompt)" (prompt-show-user-messages-body user-messages))])]
(multi-str
(string/join "\n\n" sections)
""
"_Tool schemas are sent separately and are not included in this text dump._")))

(defn handle-command! [command args {:keys [chat-id db* config messenger full-model agent all-tools instructions user-messages metrics] :as chat-ctx}]
(let [db @db*
custom-cmds (custom-commands config (:workspace-folders db))
Expand Down Expand Up @@ -782,21 +821,10 @@
msg (rules-msg config roots agent full-model all-tools)]
{:type :chat-messages
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text msg}]}]}}})
"prompt-show" (let [full-prompt (str "Instructions:\n" (f.prompt/instructions->str instructions) "\n"
"Prompt:\n" (reduce
(fn [s {:keys [content]}]
(str
s
(reduce
#(str %1 (string/replace-first (:text %2) "/prompt-show " "") "\n")
""
content)))
""
user-messages))]
{:type :chat-messages
:chats {chat-id {:messages [{:role "system"
:content [{:type :text
:text full-prompt}]}]}}})
"prompt-show" {:type :chat-messages
:chats {chat-id {:messages [{:role "system"
:content [{:type :text
:text (prompt-show-text instructions user-messages)}]}]}}}
"subagents" (let [msg (subagents-msg config)]
{:type :chat-messages
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text msg}]}]}}})
Expand Down
Loading
Loading