|
1244 | 1244 | :model full-model |
1245 | 1245 | :status :prompting})) |
1246 | 1246 |
|
| 1247 | +(def ^:private max-client-chat-id-length 256) |
| 1248 | + |
| 1249 | +(defn validate-client-chat-id |
| 1250 | + "Validates a client-supplied chat id. Returns nil when valid, otherwise an |
| 1251 | + error message string. |
| 1252 | +
|
| 1253 | + Public so non-prompt selection handlers (`chat/selectedModelChanged`, |
| 1254 | + `chat/selectedAgentChanged`) can apply the same rules to their `chat-id` |
| 1255 | + field. |
| 1256 | +
|
| 1257 | + Rejected: blank, the reserved `subagent-` prefix (used for deterministic |
| 1258 | + subagent chat ids in `eca.features.tools.agent`), embedded whitespace or |
| 1259 | + control characters (which would mangle log lines / tab-line titles), and |
| 1260 | + ids longer than `max-client-chat-id-length` characters." |
| 1261 | + [chat-id] |
| 1262 | + (cond |
| 1263 | + (string/blank? chat-id) |
| 1264 | + "chatId must be a non-blank string" |
| 1265 | + |
| 1266 | + (string/starts-with? chat-id "subagent-") |
| 1267 | + "chatId prefix 'subagent-' is reserved for server-managed subagent chats" |
| 1268 | + |
| 1269 | + (re-find #"[\s\p{Cntrl}]" chat-id) |
| 1270 | + "chatId must not contain whitespace or control characters" |
| 1271 | + |
| 1272 | + (> (count chat-id) max-client-chat-id-length) |
| 1273 | + (str "chatId must be " max-client-chat-id-length " characters or fewer"))) |
| 1274 | + |
1247 | 1275 | (defn prompt |
1248 | 1276 | [{:keys [message agent behavior chat-id contexts variant trust] :as params} db* messenger config metrics] |
1249 | | - (let [raw-agent (or agent |
1250 | | - behavior ;; backward compat: accept old 'behavior' param |
1251 | | - (-> config :chat :defaultAgent) ;; legacy |
1252 | | - (-> config :defaultAgent)) |
1253 | | - chat-id (or chat-id |
1254 | | - (let [new-id (str (random-uuid))] |
1255 | | - (swap! db* assoc-in [:chats new-id] {:id new-id}) |
1256 | | - new-id)) |
1257 | | - selected-agent (config/validate-agent-name raw-agent config) |
1258 | | - agent-config (get-in config [:agent selected-agent]) |
1259 | | - base-chat-ctx (assoc-some {:metrics metrics |
1260 | | - :config config |
1261 | | - :contexts contexts |
1262 | | - :db* db* |
1263 | | - :messenger messenger |
1264 | | - :user-content-id (lifecycle/new-content-id) |
1265 | | - :message (string/trim message) |
1266 | | - :chat-id chat-id |
1267 | | - :agent selected-agent |
1268 | | - :agent-config agent-config |
1269 | | - :trust trust |
1270 | | - :variant (or variant (:variant agent-config))} |
1271 | | - :parent-chat-id (get-in @db* [:chats chat-id :parent-chat-id])) |
1272 | | - _ (when (some? trust) |
1273 | | - (swap! db* assoc-in [:chats chat-id :trust] trust))] |
1274 | | - (try |
1275 | | - (prompt* params base-chat-ctx) |
1276 | | - (catch Exception e |
1277 | | - (logger/error e) |
1278 | | - (lifecycle/send-content! base-chat-ctx :system {:type :text |
1279 | | - :text (str "Error: " (ex-message e) "\n\nCheck ECA stderr for more details.")}) |
1280 | | - (lifecycle/finish-chat-prompt! :idle (dissoc base-chat-ctx :on-finished-side-effect)) |
1281 | | - {:chat-id chat-id |
1282 | | - :model "error" |
1283 | | - :status :error})))) |
| 1277 | + (let [provided-chat-id chat-id |
| 1278 | + invalid-id-reason (when (some? provided-chat-id) |
| 1279 | + (validate-client-chat-id provided-chat-id))] |
| 1280 | + (if invalid-id-reason |
| 1281 | + (do (logger/warn logger-tag "Rejected chat/prompt with invalid chat-id" |
| 1282 | + {:chat-id provided-chat-id :reason invalid-id-reason}) |
| 1283 | + {:chat-id provided-chat-id |
| 1284 | + :model "error" |
| 1285 | + :status :error}) |
| 1286 | + (let [raw-agent (or agent |
| 1287 | + behavior ;; backward compat: accept old 'behavior' param |
| 1288 | + (-> config :chat :defaultAgent) ;; legacy |
| 1289 | + (-> config :defaultAgent)) |
| 1290 | + chat-id (or provided-chat-id (str (random-uuid))) |
| 1291 | + ;; Atomically seed the chat record if absent and remember whether |
| 1292 | + ;; we were the ones to create it. swap-vals! returns [old new] so |
| 1293 | + ;; chat-just-created? is true iff the chat was missing pre-swap. |
| 1294 | + [old-db _] (swap-vals! db* update :chats |
| 1295 | + (fn [chats] |
| 1296 | + (if (contains? chats chat-id) |
| 1297 | + chats |
| 1298 | + (assoc chats chat-id {:id chat-id})))) |
| 1299 | + chat-just-created? (not (contains? (:chats old-db) chat-id)) |
| 1300 | + ;; Notify observers (other clients, remote SSE viewers) about a |
| 1301 | + ;; new client-initiated chat. Skipped on the legacy null-id path |
| 1302 | + ;; because the prompting client learns its id from the response. |
| 1303 | + _ (when (and provided-chat-id chat-just-created?) |
| 1304 | + (messenger/chat-opened messenger {:chat-id chat-id})) |
| 1305 | + selected-agent (config/validate-agent-name raw-agent config) |
| 1306 | + agent-config (get-in config [:agent selected-agent]) |
| 1307 | + base-chat-ctx (assoc-some {:metrics metrics |
| 1308 | + :config config |
| 1309 | + :contexts contexts |
| 1310 | + :db* db* |
| 1311 | + :messenger messenger |
| 1312 | + :user-content-id (lifecycle/new-content-id) |
| 1313 | + :message (string/trim message) |
| 1314 | + :chat-id chat-id |
| 1315 | + :agent selected-agent |
| 1316 | + :agent-config agent-config |
| 1317 | + :trust trust |
| 1318 | + :variant (or variant (:variant agent-config))} |
| 1319 | + :parent-chat-id (get-in @db* [:chats chat-id :parent-chat-id])) |
| 1320 | + _ (when (some? trust) |
| 1321 | + (swap! db* assoc-in [:chats chat-id :trust] trust))] |
| 1322 | + (try |
| 1323 | + (prompt* params base-chat-ctx) |
| 1324 | + (catch Exception e |
| 1325 | + (logger/error e) |
| 1326 | + (lifecycle/send-content! base-chat-ctx :system {:type :text |
| 1327 | + :text (str "Error: " (ex-message e) "\n\nCheck ECA stderr for more details.")}) |
| 1328 | + (lifecycle/finish-chat-prompt! :idle (dissoc base-chat-ctx :on-finished-side-effect)) |
| 1329 | + {:chat-id chat-id |
| 1330 | + :model "error" |
| 1331 | + :status :error})))))) |
1284 | 1332 |
|
1285 | 1333 | (defn tool-call-approve [{:keys [chat-id tool-call-id save]} db* messenger metrics] |
1286 | | - (let [chat-ctx {:chat-id chat-id |
1287 | | - :db* db* |
1288 | | - :metrics metrics |
1289 | | - :messenger messenger}] |
1290 | | - (tc/transition-tool-call! db* chat-ctx tool-call-id :user-approve |
1291 | | - {:reason {:code :user-choice-allow |
1292 | | - :text "Tool call allowed by user choice"}}) |
1293 | | - (when (= "session" save) |
1294 | | - (let [tool-call-name (get-in @db* [:chats chat-id :tool-calls tool-call-id :name])] |
1295 | | - (swap! db* assoc-in [:tool-calls tool-call-name :remember-to-approve?] true))))) |
| 1334 | + (if-not (get-in @db* [:chats chat-id :tool-calls tool-call-id]) |
| 1335 | + (logger/warn logger-tag "tool-call-approve ignored: unknown chat or tool-call" |
| 1336 | + {:chat-id chat-id :tool-call-id tool-call-id}) |
| 1337 | + (let [chat-ctx {:chat-id chat-id |
| 1338 | + :db* db* |
| 1339 | + :metrics metrics |
| 1340 | + :messenger messenger}] |
| 1341 | + (tc/transition-tool-call! db* chat-ctx tool-call-id :user-approve |
| 1342 | + {:reason {:code :user-choice-allow |
| 1343 | + :text "Tool call allowed by user choice"}}) |
| 1344 | + (when (= "session" save) |
| 1345 | + (let [tool-call-name (get-in @db* [:chats chat-id :tool-calls tool-call-id :name])] |
| 1346 | + (swap! db* assoc-in [:tool-calls tool-call-name :remember-to-approve?] true)))))) |
1296 | 1347 |
|
1297 | 1348 | (defn tool-call-reject [{:keys [chat-id tool-call-id]} db* messenger metrics] |
1298 | | - (let [chat-ctx {:chat-id chat-id |
1299 | | - :db* db* |
1300 | | - :metrics metrics |
1301 | | - :messenger messenger}] |
1302 | | - (tc/transition-tool-call! db* chat-ctx tool-call-id :user-reject |
1303 | | - {:reason {:code :user-choice-deny |
1304 | | - :text "Tool call rejected by user choice"}}))) |
| 1349 | + (if-not (get-in @db* [:chats chat-id :tool-calls tool-call-id]) |
| 1350 | + (logger/warn logger-tag "tool-call-reject ignored: unknown chat or tool-call" |
| 1351 | + {:chat-id chat-id :tool-call-id tool-call-id}) |
| 1352 | + (let [chat-ctx {:chat-id chat-id |
| 1353 | + :db* db* |
| 1354 | + :metrics metrics |
| 1355 | + :messenger messenger}] |
| 1356 | + (tc/transition-tool-call! db* chat-ctx tool-call-id :user-reject |
| 1357 | + {:reason {:code :user-choice-deny |
| 1358 | + :text "Tool call rejected by user choice"}})))) |
1305 | 1359 |
|
1306 | 1360 | (defn query-context |
1307 | 1361 | [{:keys [query contexts chat-id]} |
|
0 commit comments