Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(agents): Add on_llm_start and on_llm_end Lifecycle Hooks#987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
uzair330 wants to merge7 commits intoopenai:main
base:main
Choose a base branch
Loading
fromuzair330:feat/add-agent-hooks

Conversation

uzair330
Copy link

@uzair330uzair330 commentedJul 1, 2025
edited
Loading

Motivation

Currently, theAgentHooks provide valuable lifecycle events for the start/end of an agent run and for tool execution (on_tool_start/on_tool_end). However, developers lack the ability to observe the agent's execution at the language model level.

This PR introduces two new hooks,on_llm_start andon_llm_end, to provide this deeper level of observability. This change enables several key use cases:

  • Performance Monitoring: Precisely measure the latency of LLM calls.
  • Debugging & Logging: Log the exact prompts sent to and raw responses received from the model.
  • Implementing Custom Logic: Trigger actions (e.g., updating a UI, saving state) immediately before or after the agent "thinks."

Summary of Changes

  • src/agents/lifecycle.py
    Added two new async methods,on_llm_start andon_llm_end, to theAgentHooks base class, matching the existingon_*_start/on_*_end pattern.

  • src/agents/run.py
    Wrapped the call tomodel.get_response(...) in_get_new_response with invocations of the new hooks so that they fire immediately before and after each LLM call.

  • tests/test_agent_llm_hooks.py
    Added unit tests (using a mock model and spy hooks) to validate:

    1. The correct sequence ofon_start → on_llm_start → on_llm_end → on_end in a chat‑only run.
    2. The correct wrapping of tool execution in a tool‑using run:
      on_start → on_llm_start → on_llm_end → on_tool_start → on_tool_end → on_llm_start → on_llm_end → on_end.
    3. That the agent still runs without error whenagent.hooks isNone.

Usage Examples

1. Async Example (awaitable viarun)

importasynciofromtypingimportAny,Optionalfromdotenvimportload_dotenvfromagents.agentimportAgentfromagents.itemsimportModelResponse,TResponseInputItemfromagents.lifecycleimportAgentHooks,RunContextWrapperfromagents.runimportRunner# Load any OPENAI_API_KEY or other env varsload_dotenv()# --- 1. Define a custom hooks class to track LLM calls ---classLLMTrackerHooks(AgentHooks[Any]):asyncdefon_llm_start(self,context:RunContextWrapper,agent:Agent,system_prompt:Optional[str],input_items:list[TResponseInputItem],    )->None:print(f">>> [HOOK] Agent '{agent.name}' is calling the LLM with system prompt:{system_promptor'[none]'}"        )asyncdefon_llm_end(self,context:RunContextWrapper,agent:Agent,response:ModelResponse,    )->None:ifresponse.usage:print(f">>> [HOOK] LLM call finished. Tokens used:{response.usage.total_tokens}")# --- 2. Create your agent with these hooks ---my_agent=Agent(name="MyMonitoredAgent",instructions="Tell me a joke.",hooks=LLMTrackerHooks(),)# --- 3. Drive it via an async main() ---asyncdefmain():result=awaitRunner.run(my_agent,"Tell me a joke.")print(f"\nAgent output:\n{result.final_output}")if__name__=="__main__":asyncio.run(main())

2. Sync Example (blocking viarun_sync)

fromtypingimportAny,Optionalfromdotenvimportload_dotenvfromagents.agentimportAgentfromagents.itemsimportModelResponse,TResponseInputItemfromagents.lifecycleimportAgentHooks,RunContextWrapperfromagents.runimportRunner# Load any OPENAI_API_KEY or other env varsload_dotenv()# --- 1. Define a custom hooks class to track LLM calls ---classLLMTrackerHooks(AgentHooks[Any]):asyncdefon_llm_start(self,context:RunContextWrapper,agent:Agent,system_prompt:Optional[str],input_items:list[TResponseInputItem],    )->None:print(f">>> [HOOK] Agent '{agent.name}' is calling the LLM with system prompt:{system_promptor'[none]'}"        )asyncdefon_llm_end(self,context:RunContextWrapper,agent:Agent,response:ModelResponse,    )->None:ifresponse.usage:print(f">>> [HOOK] LLM call finished. Tokens used:{response.usage.total_tokens}")# --- 2. Create your agent with these hooks ---my_agent=Agent(name="MyMonitoredAgent",instructions="Tell me a joke.",hooks=LLMTrackerHooks(),)# --- 3. Drive it via an async main() ---defmain():result=Runner.run_sync(my_agent,"Tell me a joke.")print(f"\nAgent output:\n{result.final_output}")if__name__=="__main__":main()

Note

Streaming support foron_llm_start andon_llm_end is not yet implemented. These hooks currently fire only on non‑streamed (batch) LLM calls. Support for streaming invocations will be added in a future release.

Checklist

  • My code follows the style guidelines of this project (checked withruff).
  • I have added tests that prove my feature works.
  • All new and existing tests passed locally with my changes.

@seratchseratch added enhancementNew feature or request feature:core labelsJul 8, 2025
@seratch
Copy link
Member

When execute_tools_and_side_effects takes long, indeed these hooks would be helpful to understand the exact time spent by LLM. One quick thing I noticed is that probably this implementation doe not yet support streaming patterns.

@rm-openai do you think having these two hooks is good to go?

@uzair330
Copy link
Author

uzair330 commentedJul 12, 2025
edited
Loading

Thanks for the feedback—here’s a quick run‑through:

  1. Why these hooks matter for timing
    By firingon_llm_start immediately beforemodel.get_response(...) andon_llm_end the moment that call returns, we capture a clean window around the pure LLM round‑trip. Anything that happens between those two hooks (tool calls, side‑effects, post‑processing) is clearly outside that window, so long‑running tools no longer obscure where time is spent. You can record timestamps in the hooks (or push spans, metrics, logs, etc.) to drill into LLM latency vs. other work.

  2. Streaming support
    @seratch You’re correct that right now we only wrap thebatch (get_response) path. Thestreaming code (run_streamedmodel.stream_response) will need a similar hook invocation before the stream starts and once on the finalResponseCompletedEvent. I’ll follow up shortly with a small PR to add those calls.

@uzair330
Copy link
Author


New: Streaming Support foron_llm_start andon_llm_end Hooks

This update adds full support for theon_llm_start andon_llm_end hooks instreaming mode, ensuring feature parity with non-streaming execution.


1. Implementation (src/agents/run.py)

  • Hook calls are now moved inside_run_single_turn_streamed to ensure they receive the correct turn-specific data.
  • on_llm_start is triggered just before themodel.stream_response call, with the resolvedsystem_prompt.
  • on_llm_end is triggered after theResponseCompletedEvent and once the finalModelResponse is assembled.

2. Unit Tests (tests/test_agent_llm_hooks.py)

  • Added a new test:test_streamed_hooks_for_chat_scenario, which verifies that the hooks fire in the correct sequence usingRunner.run_streamed.
  • Confirms streaming and non-streaming execution paths are now consistent.

✅ Streaming Example

The hooks now work seamlessly withRunner.run_streamed, enabling real-time insight into LLM usage:

importasynciofromtypingimportAnyfromdotenvimportload_dotenvfromagents.agentimportAgentfromagents.itemsimportItemHelpers,ModelResponsefromagents.lifecycleimportAgentHooks,RunContextWrapperfromagents.runimportRunnerload_dotenv()classLLMTrackerHooks(AgentHooks[Any]):asyncdefon_llm_start(self,context:RunContextWrapper,agent:Agent,system_prompt:str,input_items:Any,    )->None:print(f"[HOOK] on_llm_start: LLM is starting, system_prompt={system_prompt}")asyncdefon_llm_end(self,context:RunContextWrapper,agent:Agent,response:ModelResponse,    )->None:total=response.usage.total_tokensifresponse.usageelseNoneprint(f"[HOOK] on_llm_end: LLM returned, total_tokens={total}")asyncdefmain():agent=Agent(name="MyStreamingAgent",instructions="You are a helpful assistant that can answer questions and perform tasks.",hooks=LLMTrackerHooks(),          )stream=Runner.run_streamed(agent,input="Create a Python script that prints 'Hello, World!'")asyncforeventinstream.stream_events():ifevent.type=="raw_response_event":continueifevent.type=="agent_updated_stream_event":print(f"[EVENT] agent_updated →{event.new_agent.name}")elifevent.type=="run_item_stream_event":item=event.itemifitem.type=="tool_call_item":print("[EVENT] tool_call_item")elifitem.type=="tool_call_output_item":print(f"[EVENT] tool_call_output_item →{item.output}")elifitem.type=="message_output_item":text=ItemHelpers.text_message_output(item)print(f"[EVENT] message_output_item →{text}")print(f"\n[RUN COMPLETE] final_output →{stream.final_output}")if__name__=="__main__":asyncio.run(main())

@uzair330
Copy link
Author

Thanks again for the valuable feedback,@seratch.
Just a gentle ping for you and@rm-openai to let you know that I've pushed the update to add full streaming support for the LLM hooks, and all CI checks are now passing.
Ready for another look when you have a moment. Thanks!

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@seratchseratchAwaiting requested review from seratch

@rm-openairm-openaiAwaiting requested review from rm-openai

At least 1 approving review is required to merge this pull request.

Assignees
No one assigned
Labels
enhancementNew feature or requestfeature:core
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

2 participants
@uzair330@seratch

[8]ページ先頭

©2009-2025 Movatter.jp