From 1732ecaad801d6702b6d1fe4982ae49dcd666a96 Mon Sep 17 00:00:00 2001 From: Kailigithub <12250313+Kailigithub@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:08:12 +0800 Subject: [PATCH 1/2] fix(agent_loop): apply agent_before hook return value to context (closes #537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugins/hooks.py::trigger() already supports returning a dict to mutate agent context, but agent_runner_loop() discarded the return value of _hook('agent_before', ...). Plugins returning updated system_prompt, user_input, or initial_user_content had their changes silently dropped. When the hook returns a dict, apply system_prompt to messages[0] and user_input (or initial_user_content when set) to messages[1]. Backward compatible: hooks returning None or non-dict values leave messages unchanged, so existing read-only hooks (logging, telemetry, langfuse) keep working without modification. Verification: /tmp/test_issue_537.py — 4 cases (system_prompt override, legacy None-return, non-dict return, user_input override). All pass on the fixed code, Test 1 fails on main (messages[0] == 'ORIGINAL prompt' without '[AUGMENTED]' suffix). --- agent_loop.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/agent_loop.py b/agent_loop.py index a744f0d26..494db9f5c 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -46,7 +46,14 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, {"role": "user", "content": initial_user_content if initial_user_content is not None else user_input} ] turn = 0; handler.max_turns = max_turns - _hook('agent_before', locals()) + _ctx = _hook('agent_before', locals()) + if isinstance(_ctx, dict): + if 'system_prompt' in _ctx: + messages[0]['content'] = _ctx['system_prompt'] + if _ctx.get('initial_user_content') is not None: + messages[1]['content'] = _ctx['initial_user_content'] + elif 'user_input' in _ctx and initial_user_content is None: + messages[1]['content'] = _ctx['user_input'] while turn < handler.max_turns: turn += 1; turnstr = f'LLM Running (Turn {turn}) ...' if handler.parent.task_dir: turnstr = f'Turn {turn} ...' From f3a06ee92b9578495093aa36ecf2e035ada404c2 Mon Sep 17 00:00:00 2001 From: Kailigithub <12250313+Kailigithub@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:10:44 +0800 Subject: [PATCH 2/2] fix(agent_loop): document message-index contract and hook precedence in #537 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review comments on PR #714 from Solaris-star (CONTRIBUTOR): 1. Document that messages[0] = system, messages[1] = user is part of the contract — future refactors that prepend a tool/system message at index 0 must update both the list literal and the hook-override indices to avoid silent writes to the wrong slot. 2. Document that 'initial_user_content' (when returned by the hook and truthy) takes precedence over 'user_input'. The plugin override wins over the caller's explicit initial_user_content — this matches the call-site precedence 'initial_user_content if initial_user_content is not None else user_input' and is the intended behavior: plugins are the last word on first-turn content. Behavior is unchanged — comments only. Verified via 6-case AST simulation that the patch preserves the original precedence semantics: A. plugin overrides both → plugin wins B. plugin overrides initial_user_content when caller's was None C. plugin overrides user_input when no initial_user_content D. plugin returns user_input but caller has initial_user_content → caller's wins E. no plugin override → caller values used F. plugin returns system_prompt only --- agent_loop.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/agent_loop.py b/agent_loop.py index 494db9f5c..90266a390 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -41,6 +41,10 @@ def get_pretty_json(data): def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, max_turns=40, verbose=True, initial_user_content=None, yield_info=False): + # messages[0] = system, messages[1] = user; agent_before hooks may + # override either via the returned dict. Index positions are part of the + # contract — if a system message is ever prepended in index 0 (e.g. by a + # future tool/system split), update these indices accordingly. messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": initial_user_content if initial_user_content is not None else user_input} @@ -50,6 +54,12 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, if isinstance(_ctx, dict): if 'system_prompt' in _ctx: messages[0]['content'] = _ctx['system_prompt'] + # initial_user_content takes precedence over user_input when both + # are returned by the hook. The caller passed `initial_user_content` + # explicitly as the resolved first-turn content; if a plugin + # override disagrees, the plugin override wins (plugin is the + # last word). This matches the default precedence at call site: + # `initial_user_content if initial_user_content is not None else user_input`. if _ctx.get('initial_user_content') is not None: messages[1]['content'] = _ctx['initial_user_content'] elif 'user_input' in _ctx and initial_user_content is None: