Skip to content

Python: feat(core): add max_duration_seconds bound and stop_reason signal to tool loop (#7587) - #7772

Open
Thota Sai Karthik (karthik-0306) wants to merge 1 commit into
microsoft:mainfrom
karthik-0306:fix-issue-7587
Open

Python: feat(core): add max_duration_seconds bound and stop_reason signal to tool loop (#7587)#7772
Thota Sai Karthik (karthik-0306) wants to merge 1 commit into
microsoft:mainfrom
karthik-0306:fix-issue-7587

Conversation

@karthik-0306

Copy link
Copy Markdown
Contributor

Motivation & Context

Function invocation loops in FunctionInvocationLayer (_tools.py) currently allow capping LLM roundtrips via max_iterations and total function calls via max_function_calls, but lack a wall-clock time limit. Unattended or complex agent runs can execute tools repeatedly and stall for long periods without a bounded total duration.

Additionally, callers currently have no programmatic way to determine why a function invocation run ended (e.g. normal completion vs hitting max_iterations or a tool limit).

This PR addresses #7587 by introducing max_duration_seconds to FunctionInvocationConfiguration and surfacing a _agent_framework_stop_reason signal on ChatResponse.additional_properties.

Description & Review Guide

What are the major changes?

  • max_duration_seconds Config Field: Added max_duration_seconds: float | None to FunctionInvocationConfiguration (TypedDict) and normalized validation (> 0 or None).
  • Wall-Clock Budget Tracking: Added budget_state.setdefault("start_time", perf_counter()) in get_response so duration is measured cumulatively across human approval round-trips.
  • Graceful Degradation Path: When max_duration_seconds is exceeded mid-loop (checked after each tool batch), further tool calls are disabled (tool_choice = "none") and the model is forced to produce a final text response, reusing the established max_function_calls degradation path.
  • stop_reason Signal: Surface _agent_framework_stop_reason in ChatResponse.additional_properties with values "completed", "max_iterations", or "max_duration_seconds".
  • Precedence Control: Used setdefault("stop_reason", ...) so the first triggered condition in execution order claims the reason (duration check runs before the call-count check).
  • Streaming Finalizer Integration: Wrapped ResponseStream with _finalize_with_stop_reason so streaming callers receive _agent_framework_stop_reason on the final ChatResponse without altering individual streaming update counts.

What is the impact of these changes?

  • Provides a strict wall-clock safeguard against runaway function invocation loops.
  • Enables callers to inspect response.additional_properties["_agent_framework_stop_reason"] to programmatically handle how a run concluded.
  • Backward-compatible: default max_duration_seconds is None (unlimited).

What do you want reviewers to focus on?

  • Verify the setdefault("stop_reason", ...) logic in both non-streaming (_get_response_with_function_invocation) and streaming (_stream_response_with_function_invocation) loops.
  • Confirm that budget_state.setdefault("start_time", ...) in get_response properly preserves start time across approval resumes for cumulative timing.
  • Review the ResponseStream finalizer wrapper in get_response ensuring streaming updates remain clean and untouched.

Related Issue

Fixes #7587

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds duration bounds and machine-readable termination reasons to Python function-invocation loops.

Changes:

  • Adds and validates max_duration_seconds.
  • Tracks stop reasons across streaming and non-streaming paths.
  • Adds tests and changelog documentation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
python/packages/core/agent_framework/_tools.py Implements duration tracking and stop reasons.
python/packages/core/tests/core/test_function_invocation_logic.py Tests duration limits and termination signals.
python/CHANGELOG.md Documents the new behavior.
Suppressed comments (2)

python/packages/core/agent_framework/_tools.py:3528

  • The streaming path has the same enforcement gap: approved calls are replayed before this check, while the call-dropping/fallback logic at lines 3449-3466 recognizes only max_function_calls. Consequently, an expired approval or a provider-emitted call despite tool_choice="none" can still execute. Include duration expiry in a shared pre-execution and fallback predicate.
                if (
                    max_duration_seconds is not None
                    and (perf_counter() - budget_state["start_time"]) >= max_duration_seconds
                ):

python/packages/core/agent_framework/_tools.py:3518

  • The streaming branch also leaks the internal action name "stop" as a public stop reason. This is outside the documented value set and differs from approval-time error exhaustion, which reports completed. Use the same documented semantic reason for consecutive-error exhaustion in both paths.
                budget_state.setdefault("stop_reason", "stop")

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/packages/core/agent_framework/_tools.py
Comment thread python/packages/core/agent_framework/_tools.py
Comment thread python/packages/core/agent_framework/_tools.py
Comment thread python/packages/core/tests/core/test_function_invocation_logic.py
Comment thread python/packages/core/agent_framework/_tools.py
@github-actions github-actions Bot changed the title feat(core): add max_duration_seconds bound and stop_reason signal to tool loop (#7587) Python: feat(core): add max_duration_seconds bound and stop_reason signal to tool loop (#7587) Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Feature]: Bound an agent run by duration (and by spend), not only by iteration and call count

2 participants