From d6b7e839eb65ccbcfd4063ad704f59bb94b183f3 Mon Sep 17 00:00:00 2001 From: Juha Itkonen Date: Sat, 20 Jun 2026 09:06:09 +0300 Subject: [PATCH] Include parent chat id in subagent postRequest hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose parent_chat_id in subagent postRequest payloads so hooks can distinguish primary chat completions from subagent completions without changing postRequest semantics. 🤖 Generated with [ECA](https://eca.dev) (openai/gpt-5.5 - high) Co-Authored-By: eca-agent --- CHANGELOG.md | 1 + docs/config/hooks.md | 2 +- src/eca/features/chat/lifecycle.clj | 7 +++++-- test/eca/features/hooks_test.clj | 8 ++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3adc2e47..4504564c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Bugfix: subagent `postRequest` hooks now include `parent_chat_id`, letting hooks distinguish primary and subagent completions. (#505) - Bugfix: clearer rejected tool-call result wording so models no longer assume a rejected edit was applied; it now states the call did not run and changed nothing. (#507) - Bugfix: `ask_user` normalizes the `options` argument so a malformed value (e.g. a stringified options an LLM sometimes emits) no longer reaches clients as broken choices. Accepts string/object arrays, recovers a JSON-encoded string, and drops unusable input. diff --git a/docs/config/hooks.md b/docs/config/hooks.md index f9a89773b..6505f0769 100644 --- a/docs/config/hooks.md +++ b/docs/config/hooks.md @@ -272,7 +272,7 @@ Fires after a primary-agent prompt finishes. Also runs for subagents. Primary us !!! note `postRequest` fires only after LLM responses. Display-only commands (`/hooks`, `/model`, `/costs`) and compaction prompts (`/compact` and auto-compact) do **not** trigger it — use [`postCompact`](#postcompact) for compaction. -- **Input adds** — `response` (last assistant text) and `follow_up_active` (`true` when this turn was triggered by a previous `followUp`). +- **Input adds** — `response` (last assistant text), `follow_up_active` (`true` when this turn was triggered by a previous `followUp`), and, for subagents, `parent_chat_id`. - **Honored output**: - `followUp` — start a new LLM turn after this one (see [follow-up turns](#follow-up-turns)). - `systemMessage`, `suppressOutput`. diff --git a/src/eca/features/chat/lifecycle.clj b/src/eca/features/chat/lifecycle.clj index 8437f6102..64ba2f068 100644 --- a/src/eca/features/chat/lifecycle.clj +++ b/src/eca/features/chat/lifecycle.clj @@ -325,11 +325,14 @@ base-hook-data (assoc-some (f.hooks/chat-hook-data db chat-ctx) :response response :follow-up-active (boolean follow-up-active?)) + post-request-hook-data (assoc-some base-hook-data + :parent-chat-id (when subagent? + (db/parent-chat-id db chat-id))) cb {:on-before-action (partial notify-before-hook-action! chat-ctx) :on-after-action (fn [result] (notify-after-hook-action! chat-ctx result) (swap! results* conj result))} - _ (f.hooks/trigger-if-matches! :postRequest base-hook-data cb db config) + _ (f.hooks/trigger-if-matches! :postRequest post-request-hook-data cb db config) ;; A successful continue:false on a postRequest hook stops the turn, so ;; the remaining relevant hooks must not run. For subagents this means ;; subagentPostRequest is skipped, otherwise it could emit side effects @@ -337,7 +340,7 @@ post-request-stopped? (boolean (some f.hooks/successful-continue-false? @results*)) _ (when (and subagent? (not post-request-stopped?)) (f.hooks/trigger-if-matches! :subagentPostRequest - (assoc base-hook-data :parent-chat-id (db/parent-chat-id db chat-id)) + post-request-hook-data cb db config)) diff --git a/test/eca/features/hooks_test.clj b/test/eca/features/hooks_test.clj index c7668bcdf..2454cc89f 100644 --- a/test/eca/features/hooks_test.clj +++ b/test/eca/features/hooks_test.clj @@ -1864,10 +1864,9 @@ :actions [{:type "shell" :shell "echo pr"}]} "spr" {:type "subagentPostRequest" :actions [{:type "shell" :shell "echo spr"}]}}}) - (let [fired-types* (atom [])] + (let [payloads* (atom [])] (with-redefs [f.hooks/run-shell-cmd (fn [{:keys [input]}] - (let [data (json/parse-string input true)] - (swap! fired-types* conj (:hook_type data))) + (swap! payloads* conj (json/parse-string input true)) {:exit 0 :out "" :err nil})] (lifecycle/finish-chat-prompt! :idle {:db* (h/db*) :config (h/config) @@ -1875,7 +1874,8 @@ :agent "explorer" :messenger (h/messenger) :metrics (h/metrics)})) - (is (= ["postRequest" "subagentPostRequest"] @fired-types*))))) + (is (= ["postRequest" "subagentPostRequest"] (mapv :hook_type @payloads*))) + (is (= ["parent-1" "parent-1"] (mapv :parent_chat_id @payloads*)))))) (deftest subagent-postrequest-skipped-when-postrequest-stops-test (testing "a successful postRequest continue:false stops the turn and skips subagentPostRequest"