Skip to content

fix(agent_loop): apply agent_before hook return value to context (closes #537) - #714

Open
Kailigithub wants to merge 1 commit into
lsdefine:mainfrom
Kailigithub:fix/issue-537-agent-before-hook-return
Open

fix(agent_loop): apply agent_before hook return value to context (closes #537)#714
Kailigithub wants to merge 1 commit into
lsdefine:mainfrom
Kailigithub:fix/issue-537-agent-before-hook-return

Conversation

@Kailigithub

Copy link
Copy Markdown
Contributor

Problem

plugins/hooks.py::trigger() already supports returning a dict to mutate
agent context (it rebinds ctx = r when a callback returns a dict).
But agent_runner_loop() in agent_loop.py discarded the return value
of _hook('agent_before', locals()). Plugins that wanted to override
system_prompt, user_input, or initial_user_content had their
changes silently dropped.

This is the same class of issue called out in #537.

Fix

Capture the return value of _hook('agent_before', locals()):

  • When a dict is returned, apply system_prompt / user_input /
    initial_user_content overrides to the messages list.
  • When the hook returns None (the historical contract) or a non-dict
    value, the loop proceeds with the original arguments.
  • initial_user_content takes precedence over user_input, preserving
    the existing argument-resolution order at the top of the function.
 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:

Backward compatibility

Existing read-only hooks (plugins/langfuse_tracing.py::_on_agent_before
reads user_input; plugins/project_mode.py::inject_project_context
mutates ctx['messages'] in place) keep working without modification:

  • langfuse_tracing._on_agent_before returns nothing → _ctx is None
    isinstance(_ctx, dict) is False → messages unchanged.
  • project_mode.inject_project_context mutates ctx['messages'] in
    place and returns nothing → same path as above.

No existing plugin is broken by this change.

Verification

/tmp/test_issue_537.py — 4 standalone tests against
agent_runner_loop using a stub client and BaseHandler subclass:

# Scenario Result on main Result on fix
1 Hook returns dict with system_prompt override messages[0] unchanged messages[0] updated
2 Hook returns None (legacy contract) messages unchanged messages unchanged
3 Hook returns a non-dict (e.g. string) messages unchanged messages unchanged
4 Hook returns dict with user_input override messages[1] unchanged messages[1] updated

Three-step dance confirms Test 1 fails on the unfixed code
(messages[0] == 'ORIGINAL prompt') and passes with the fix
(messages[0] == 'ORIGINAL prompt [AUGMENTED]').

$ python3 -m py_compile agent_loop.py
✓ py_compile OK

$ python3 /tmp/test_issue_537.py
✓ Test 1: system_prompt override applied via return value
✓ Test 2: legacy hook returning None leaves messages unchanged
✓ Test 3: non-dict return value is ignored (backward compatible)
✓ Test 4: user_input override applied via return value
All 4 tests PASSED.

ruff check agent_loop.py: 40 pre-existing style findings (compact
imports, semicolon-separated statements — intentional per
CONTRIBUTING.md's "Compact and visually uniform"). The fix introduces
zero new ruff findings (verified by git stash + re-check).

Scope

  • 1 file changed: agent_loop.py
  • 8 insertions, 1 deletion
  • No new tests added to tests/ (the repo currently has no pytest
    suite); verification artifact lives at /tmp/test_issue_537.py and
    is documented above for the maintainer's review.

Closes #537

 lsdefine#537)

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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Plugin hooks cannot modify context due to ignored return value

1 participant