Skip to content

docs(examples): add force_tool_first_turn hook example#2014

Open
gold-silver-copper wants to merge 1 commit into
mainfrom
example/force-tool-first-turn
Open

docs(examples): add force_tool_first_turn hook example#2014
gold-silver-copper wants to merge 1 commit 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.
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