Skip to content

docs(examples): add force_tool_first_turn hook example - #2014

Merged
gold-silver-copper merged 3 commits into
mainfrom
example/force-tool-first-turn
Jul 9, 2026
Merged

docs(examples): add force_tool_first_turn hook example#2014
gold-silver-copper merged 3 commits into
mainfrom
example/force-tool-first-turn

Conversation

@gold-silver-copper

Copy link
Copy Markdown
Contributor

Summary

Adds a runnable example, examples/force_tool_first_turn, that demonstrates a real per-turn RequestPatch footgun in the hook system and its fix.

The pattern

A hook can force the model to call a tool by returning Flow::patch_request(RequestPatch::new().tool_choice(ToolChoice::Required)) on StepEvent::CompletionCall. But RequestPatch is per-turn and non-stickyCompletionCall re-fires on every turn — so applying Required unconditionally forces a tool call on every turn. The model never reaches a turn where it can stop calling tools and write the final answer, so the run loops until max_turns and fails with PromptError::MaxTurnsError.

The fix is to gate the patch on the turn index:

if matches!(event, StepEvent::CompletionCall { .. }) && ctx.turn() == 1 {
    return Flow::patch_request(RequestPatch::new().tool_choice(ToolChoice::Required));
}
Flow::cont()

The example runs the footgun first (and catches the resulting MaxTurnsError), then runs the fix, so the difference is visible end-to-end.

Changes

  • examples/force_tool_first_turn/{Cargo.toml,src/main.rs} — the example (auto-registered via the examples/* workspace glob).
  • examples/README.md — one table row for the new example.
  • Cargo.lock — the new package entry.

No production code changes.

Validation

  • cargo check -p force_tool_first_turn — clean.
  • cargo clippy -p force_tool_first_turn — clean.
  • cargo fmt -p force_tool_first_turn — clean.

Requires OPENAI_API_KEY to run (an example, not a test).

Demonstrates a per-turn RequestPatch footgun and its fix. A RequestPatch is
per-turn and re-fires on every turn, so an AgentHook that patches
tool_choice = Required on every CompletionCall forces a tool call on every turn
— the model never gets a turn to stop calling tools and answer, so the run loops
until max_turns and fails with MaxTurnsError.

The example runs that footgun first (catching the MaxTurnsError), then the fix:
gate the patch on ctx.turn() == 1 so the tool is forced only on the first turn
and later turns resolve normally. Registered as a workspace example and listed in
examples/README.md. Requires OPENAI_API_KEY to run.
@gold-silver-copper
gold-silver-copper force-pushed the example/force-tool-first-turn branch from ba3da9e to cb9ce7c Compare July 9, 2026 00:12
@gold-silver-copper
gold-silver-copper added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit cc4808c Jul 9, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 11, 2026
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.

1 participant