Skip to content

feat(lifecycle): add on_turn_start / on_turn_end hooks with TurnControl#2911

Closed
adityasingh2400 wants to merge 3 commits intoopenai:mainfrom
adityasingh2400:feat/turn-lifecycle-hooks-2671-v2
Closed

feat(lifecycle): add on_turn_start / on_turn_end hooks with TurnControl#2911
adityasingh2400 wants to merge 3 commits intoopenai:mainfrom
adityasingh2400:feat/turn-lifecycle-hooks-2671-v2

Conversation

@adityasingh2400
Copy link
Copy Markdown
Contributor

@adityasingh2400 adityasingh2400 commented Apr 16, 2026

Summary

Adds turn-level lifecycle hooks to RunHooksBase and AgentHooksBase, addressing #2671.

What's new

  • on_turn_start(context, agent, turn_number) — fires before the LLM is called for each turn
  • on_turn_end(context, agent, turn_number) — fires after all tool calls for a turn complete
  • TurnControl return type for on_turn_start: returning "stop" halts the run gracefully (raises MaxTurnsExceeded) before the model is called; returning None or "continue" proceeds normally

Addressing reviewer feedback

Hooks can raise an exception if something wrong but they do not affect the agent loop orchestration in a more granular way many developers wish.
@seratch

on_turn_start now returns TurnControl ("stop" | "continue" | None), giving callers direct control over the loop — not just observation. This allows patterns like:

class BudgetHooks(RunHooks):
    def __init__(self, max_turns: int):
        self.max_turns = max_turns

    async def on_turn_start(self, context, agent, turn_number) -> TurnControl:
        if turn_number > self.max_turns:
            return "stop"  # halts before the LLM call
        return None

    async def on_turn_end(self, context, agent, turn_number):
        print(f"Turn {turn_number} complete. Tokens used: {context.usage.total_tokens}")

When "stop" is returned, MaxTurnsExceeded is raised — consistent with the existing max_turns limit, so callers handle it the same way.

Files changed

File Change
src/agents/lifecycle.py on_turn_start return type → Union[TurnControl, None]; docstrings updated
src/agents/run.py Non-streaming loop checks return value; raises MaxTurnsExceeded on "stop"
src/agents/run_internal/run_loop.py Streaming loop checks return value; enqueues QueueCompleteSentinel on "stop"
src/agents/__init__.py Exports TurnControl, RunHooksBase, AgentHooksBase
tests/test_turn_lifecycle_hooks.py 10 tests (6 existing + 4 new TurnControl cases)

Closes #2671

Ubuntu and others added 2 commits April 14, 2026 22:34
…ase (openai#2671)

Both RunHooksBase and AgentHooksBase get two new hook methods:

- on_turn_start(context, agent, turn_number): fires before each LLM call
- on_turn_end(context, agent, turn_number): fires after all tool calls for
  the turn complete (i.e. just before the next-step decision)

Turn numbers are 1-indexed and increment each time through the agent
loop, regardless of handoffs.  The hooks are called in both the sync
and streaming code paths.  Agent-level hooks on agent.hooks are also
called, matching the existing on_tool_start/on_tool_end pattern.

Closes openai#2671
- Remove unnecessary f-string prefixes from on_llm_start/on_llm_end (Ruff F541)
- Add missing docstrings to class methods to improve docstring coverage
- Add docstring to OrderTrackingHooks inner class
@github-actions github-actions bot added enhancement New feature or request feature:core labels Apr 16, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05a4f15d77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/run.py
Comment on lines +1107 to +1111
await asyncio.gather(
hooks.on_turn_end(context_wrapper, current_agent, current_turn),
(
current_agent.hooks.on_turn_end(
context_wrapper, current_agent, current_turn
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Defer turn-end hook until interrupted turn actually completes

In the sync runner, on_turn_end is fired immediately after run_single_turn returns, even when next_step is NextStepInterruption (tool approval pending). In that path the turn has not actually finished—resolve_interrupted_turn continues the same turn later—so this reports a false turn completion and the hook is never re-fired at the real end. Any hook that compacts state, records per-turn metrics, or enforces turn-level cancellation will run too early for approval-based workflows.

Useful? React with 👍 / 👎.

Comment on lines +923 to +927
await asyncio.gather(
hooks.on_turn_end(context_wrapper, current_agent, current_turn),
(
current_agent.hooks.on_turn_end(
context_wrapper, current_agent, current_turn
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid emitting turn-end for paused streaming turns

The streaming loop has the same premature on_turn_end call before branching on turn_result.next_step. If the turn pauses with NextStepInterruption, the hook still fires even though approvals and remaining tool execution happen only after resume, so observers get an incorrect "turn complete" signal and never receive a true completion callback for that turn.

Useful? React with 👍 / 👎.

@seratch seratch marked this pull request as draft April 17, 2026 02:57
@seratch
Copy link
Copy Markdown
Member

seratch commented Apr 17, 2026

Adding more hooks may be worth considering, but I don't think this could be a solution for the mentioned issue. Hooks can raise an exception if something wrong but they do not affect the agent loop orchestration in a more granular way many developers wish.

Address seratch's review feedback on openai#2911: hooks that only observe
cannot affect agent loop orchestration. This commit adds a TurnControl
return type ('continue' | 'stop') so on_turn_start can now halt the
run before the LLM is called for that turn.

Changes:
- lifecycle.py: on_turn_start now returns Union[TurnControl, None]
  (None and 'continue' are equivalent; 'stop' halts the loop)
- run.py (non-streaming path): checks return value; raises
  MaxTurnsExceeded with descriptive message on 'stop'
- run_internal/run_loop.py (streaming path): checks return value;
  signals QueueCompleteSentinel on 'stop'
- __init__.py: exports TurnControl, RunHooksBase, AgentHooksBase
- tests: 4 new test cases covering stop-on-turn-N, stop-on-turn-1,
  explicit 'continue', and agent-level stop

The MaxTurnsExceeded raise on 'stop' keeps behaviour consistent with
the existing max_turns limit: callers can catch and inspect
.run_data if needed.
@adityasingh2400 adityasingh2400 force-pushed the feat/turn-lifecycle-hooks-2671-v2 branch from 05a4f15 to f1cafcd Compare April 17, 2026 03:12
@adityasingh2400 adityasingh2400 changed the title feat(lifecycle): add on_turn_start and on_turn_end hooks to RunHooksBase feat(lifecycle): add on_turn_start / on_turn_end hooks with TurnControl Apr 17, 2026
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback @seratch! You're right — pure observation hooks don't solve the orchestration-control use case.

I've updated this PR so that on_turn_start now returns a TurnControl value ("stop" | "continue" | None). Returning "stop" halts the run before the LLM is called for that turn, raising MaxTurnsExceeded (consistent with the existing max_turns behaviour). Returning None or "continue" proceeds as before.

This gives callers actual control over the loop — e.g. a custom turn budget, an external kill-switch, or circuit-breaking based on context.usage. All 10 tests pass including 4 new TurnControl cases.

@seratch
Copy link
Copy Markdown
Member

seratch commented Apr 17, 2026

Thanks for the update. However, this is still not the direction we'd like to pursue, so let us close this now. Also, all the PRs you're sending to this repo have conflicts with the latest main branch. If you're interested in sending further PRs to this project, please make sure those do not have conflicts.

@seratch seratch closed this Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature:core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: better support for agent state changes between turns

2 participants