Skip to content

Conversation

@ak684
Copy link
Contributor

@ak684 ak684 commented Dec 30, 2025

follow up to: #1467

fixes #1527

  • Implement additional_context injection in UserPromptSubmit hooks

    • Hook context is appended to MessageEvent.extended_content
    • Context is appended to MessageEvent.extended_content and included in LLM messages
  • Implement stop hook integration in conversation run loop

    • Stop hooks can deny premature agent completion
    • Feedback from hooks is injected as a user message
    • Agent continues running after stop hook denial
  • Add comprehensive tests for both features

    • Tests for context appearing in extended_content
    • Tests for context appearing in to_llm_message() output
    • Tests for stop hook denial with feedback injection
    • Integration tests for full conversation loop with stop hooks
  • Add advanced hooks example (34_hooks_advanced.py)

    • Demonstrates UserPromptSubmit with additional_context
    • Demonstrates Stop hook with conditional denial

Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:ff9c2ef-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-ff9c2ef-python \
  ghcr.io/openhands/agent-server:ff9c2ef-python

All tags pushed for this build

ghcr.io/openhands/agent-server:ff9c2ef-golang-amd64
ghcr.io/openhands/agent-server:ff9c2ef-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:ff9c2ef-golang-arm64
ghcr.io/openhands/agent-server:ff9c2ef-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:ff9c2ef-java-amd64
ghcr.io/openhands/agent-server:ff9c2ef-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:ff9c2ef-java-arm64
ghcr.io/openhands/agent-server:ff9c2ef-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:ff9c2ef-python-amd64
ghcr.io/openhands/agent-server:ff9c2ef-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:ff9c2ef-python-arm64
ghcr.io/openhands/agent-server:ff9c2ef-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:ff9c2ef-golang
ghcr.io/openhands/agent-server:ff9c2ef-java
ghcr.io/openhands/agent-server:ff9c2ef-python

About Multi-Architecture Support

  • Each variant tag (e.g., ff9c2ef-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., ff9c2ef-python-amd64) are also available if needed

@github-actions
Copy link
Contributor

github-actions bot commented Dec 30, 2025

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py2599762%148, 150–151, 172, 202, 205–206, 220–221, 244, 249, 291–292, 296–297, 342, 350, 353–357, 364–365, 368, 377–378, 381, 388, 409, 412, 416–418, 425–427, 430, 439, 455, 457, 459, 463, 465–467, 469, 471, 477–478, 491–492, 494, 496, 500–503, 520–521, 527, 532, 535, 537, 547, 549–551, 569, 571, 575, 580, 585, 590–593, 599, 602, 606, 609, 611–613, 615, 633, 635, 649, 653, 661, 677–678, 682, 684, 686, 692–693
openhands-sdk/openhands/sdk/hooks
   conversation_hooks.py13411315%35–37, 41, 46, 49–50, 53–54, 57–58, 61–62, 66–67, 69–70, 73–77, 79, 84–86, 90–92, 94, 102–103, 106–114, 116–117, 119–121, 124–128, 131–135, 137, 144–146, 154–155, 158–162, 164, 168–170, 174–176, 178, 185–187, 191, 200, 204–206, 210–212, 216–219, 223–226, 230–231, 233, 236–238, 241–245, 248–251, 253, 263, 269, 274
TOTAL14484686952% 

@ak684 ak684 force-pushed the ak684/featsdk-complete-hooks-implementation-with-additional-context-and-stop-hook-13 branch from 322a14d to 4c36ba1 Compare December 30, 2025 15:41
…stop hook

- Implement additional_context injection in UserPromptSubmit hooks
  - Hook context is appended to MessageEvent.extended_content
  - Context flows through condensation and is included in LLM messages

- Implement stop hook integration in conversation run loop
  - Stop hooks can deny premature agent completion
  - Feedback from hooks is injected as user message with [Stop hook feedback] prefix
  - Agent continues running after stop hook denial

- Add comprehensive tests for both features
  - Tests for context appearing in extended_content and to_llm_message()
  - Tests for stop hook denial with feedback injection
  - Integration tests for full conversation loop with stop hooks

- Add advanced hooks example (34_hooks_advanced.py)
@ak684 ak684 force-pushed the ak684/featsdk-complete-hooks-implementation-with-additional-context-and-stop-hook-13 branch from 4c36ba1 to bda858e Compare December 30, 2025 15:46
@enyst enyst self-requested a review December 30, 2025 15:59
@rbren
Copy link
Contributor

rbren commented Dec 30, 2025

This likely solves #1527!

Comment on lines 76 to 91
config = HookConfig.from_dict(
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [{"type": "command", "command": str(context_script)}],
}
],
"Stop": [
{
"hooks": [{"type": "command", "command": str(stop_script)}],
}
],
}
}
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: could we make this a HookConfig a pydantic class that contains UserPromptSubmit Stop, etc, which are other pydantic class, so we can have stricter type check instead of putting together a dict like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great idea. I’ll plan on implementing it in a follow-up PR so that we can keep this PR’s scope focused.

ak684 and others added 3 commits January 2, 2026 04:11
- Move 33_hooks.py to 33_hooks/ folder with separate scripts
- Split into basic_hooks.py and advanced_hooks.py
- Remove main() wrapper to match codebase convention (31/33 examples)
- Extract shell scripts to scripts/ directory for reusability
- Add README.md documenting hook types and usage

Addresses review feedback:
- Consolidate hook examples into folder structure
- Simplify examples by removing main() wrapper
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OpenHands please send a PR to OpenHands/docs repo to update the old 33_hooks.py to this new one

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on it! xingyaoww can track my progress at all-hands.dev

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've successfully created a PR to the OpenHands/docs repo to update the hooks documentation with the new 33_hooks.py example.

Summary

PR Created: OpenHands/docs#230

Changes Made

Updated sdk/guides/hooks.mdx to reflect the new hooks example from the software-agent-sdk:

  1. Updated the Python example code - Replaced the old inline script creation approach with the new structure that uses external hook scripts in a hook_scripts/ directory

  2. Added documentation for all four hook types:

    • PreToolUse: Block dangerous commands before execution
    • PostToolUse: Log tool usage after execution
    • UserPromptSubmit: Inject context into user messages
    • Stop: Enforce task completion criteria
  3. Added expandable accordion sections showing each hook script:

    • block_dangerous.sh
    • log_tools.sh
    • inject_git_context.sh
    • require_summary.sh
  4. Added a Hook Types reference table documenting when each hook runs and whether it can block operations

  5. Updated the GitHub link to point to the new directory structure (examples/01_standalone_sdk/33_hooks/)

The PR cross-references the related SDK PR (#1547).

View full conversation

xingyaoww pushed a commit to OpenHands/docs that referenced this pull request Jan 2, 2026
Update the hooks documentation to reflect the new 33_hooks.py example that
demonstrates all four hook types:
- PreToolUse: Block dangerous commands
- PostToolUse: Log tool usage
- UserPromptSubmit: Inject context into user messages
- Stop: Enforce task completion criteria

The new example uses external hook scripts in a hook_scripts/ directory
and includes a Hook Types reference table.

Related to: OpenHands/software-agent-sdk#1547

Co-authored-by: openhands <openhands@all-hands.dev>
# Run UserPromptSubmit hooks for user messages
if isinstance(event, MessageEvent) and event.source == "user":
self._handle_user_prompt_submit(event)
callback_event = self._handle_user_prompt_submit(event)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OpenHands, does this mean we will modify the event passed to the callback? Does it break the pattern we currently have for on_event? Ideally we should never mutate event passed to on_event, please investigate. Do not modify any code

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on it! xingyaoww can track my progress at all-hands.dev

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answer to Your Question

Does this modify the event passed to the callback? Does it break the on_event pattern?

No Mutation Occurs

The original event is NOT mutated. Events are frozen Pydantic models (verified by test_event_immutability.py). When additional_context needs to be injected, a new MessageEvent is created:

# MessageEvent is frozen, so create a new one
event = MessageEvent(
    source=event.source,
    llm_message=event.llm_message,
    ...
    extended_content=new_extended_content,
)

Pattern Concern

The current on_event pattern is that all callbacks in a composed chain receive the same event (via compose_callbacks). The PR's design intends to pass a different (modified) event to downstream callbacks via original_callback.

However, there's an integration issue: in LocalConversation.__init__, create_hook_callback is called without passing original_callback, so it defaults to None. This means the modified event is created but never actually passed to downstream callbacks - it's lost.

Summary

  1. No mutation - Events are frozen; a new event is created instead
  2. Pattern concern is valid - If original_callback were properly wired, downstream callbacks would receive a different event than what was originally emitted, which departs from the current "all callbacks see the same event" pattern
  3. Current integration doesn't work as intended - The modified event with additional_context is created but lost because original_callback is None in the actual LocalConversation setup. The tests pass only because they explicitly provide original_callback=capture_callback.

The design intent seems to be that the hook processor should intercept events, potentially modify them, and pass the modified version downstream - which is architecturally different from the current pattern.

View full conversation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, there's an integration issue: in LocalConversation.init, create_hook_callback is called without passing original_callback, so it defaults to None. This means the modified event is created but never actually passed to downstream callbacks - it's lost.

@ak684 is this intentional?

Copy link
Contributor Author

@ak684 ak684 Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to pass to callback, also added test

Copy link
Collaborator

@enyst enyst Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingyaoww I wonder WDYT about this solution?

@ak684
Please let me take a step back here and look at our options. We have the goal to inject some extra content in the user message, just like a keyword-skill.

  1. modify the event
  2. add a new event (this PR’s solution)
  3. intervene before the user message event is created

1 and 2 are after the event was created, and saved in EventLog (I think it was saved too?)

For 2, we then have a question:
a. do we notify listeners about the new event
b. or about both?

Also for 2, I think we have another question that I’m most interested in:

  • do we send to the LLM only the second, or do we send both?
  • (IMHO we should send only the second, since it seems to me it contains all content of the first, too; or make it have only the extended content)

ak684 added 2 commits January 3, 2026 04:10
Added tests verifying LocalConversation correctly wires hook callbacks
to event persistence via original_callback parameter.
Use grep on raw JSON input instead of jq for detecting dangerous commands.
This makes the example work out of the box without requiring jq installation.
@openhands-ai
Copy link

openhands-ai bot commented Jan 3, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • [Optional] Docs example

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1547 at branch `ak684/featsdk-complete-hooks-implementation-with-additional-context-and-stop-hook-13`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

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.

Feature Request: Add Stop Hook to prevent early agent termination

5 participants