Uh oh!
There was an error while loading. Please reload this page.
Python: feat(core): add max_duration_seconds bound and stop_reason signal to tool loop (#7587) - #7772
Conversation
There was a problem hiding this comment.
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 despitetool_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 reportscompleted. 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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
0ef8220 to
05ce001CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…eason to AgentResponse
- **agent-framework-core**: Refactored _apply_batch_limit_decision to compute perf_counter exactly once per decision point, eliminating the structural fragility where the threshold check and log message used separate clock samples. - **agent-framework-core**: Re-ordered limit checking in Phase 1 to execute before approval response resolution, successfully preventing execution during approved replays when limits are reached. A post-approval check ensures consecutive error limits (�ction == stop) remain handled. - **agent-framework-core**: Rewrote 6 tests in est_function_invocation_logic.py that used a fragile call_count mock. The tests now use a mutable clock array that advances directly during the tool execution semantic step, providing true robustness against internal engine refactors. Note: The fallback response trigger (_ensure_function_invocation_limit_fallback_response) remains scoped strictly to the function call limit, preserving pre-existing behavior. Expanding this to cover consecutive errors (�ction == stop) or duration timeouts is intentionally left out of scope for this fix.
… streaming limit decision, fix double-counted approval calls against max_function_calls
Motivation & Context
Function invocation loops in
FunctionInvocationLayer(_tools.py) currently allow capping LLM roundtrips viamax_iterationsand total function calls viamax_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_iterationsor a tool limit).This PR addresses #7587 by introducing
max_duration_secondstoFunctionInvocationConfigurationand surfacing a_agent_framework_stop_reasonsignal onChatResponse.additional_properties.Description & Review Guide
What are the major changes?
max_duration_secondsConfig Field: Addedmax_duration_seconds: float | NonetoFunctionInvocationConfiguration(TypedDict) and normalized validation (> 0orNone).budget_state.setdefault("start_time", perf_counter())inget_responseso duration is measured cumulatively across human approval round-trips.max_duration_secondsis 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 establishedmax_function_callsdegradation path.stop_reasonSignal: Surface_agent_framework_stop_reasoninChatResponse.additional_propertieswith values"completed","max_iterations", or"max_duration_seconds".setdefault("stop_reason", ...)so the first triggered condition in execution order claims the reason (duration check runs before the call-count check).ResponseStreamwith_finalize_with_stop_reasonso streaming callers receive_agent_framework_stop_reasonon the finalChatResponsewithout altering individual streaming update counts.What is the impact of these changes?
response.additional_properties["_agent_framework_stop_reason"]to programmatically handle how a run concluded.max_duration_secondsisNone(unlimited).What do you want reviewers to focus on?
setdefault("stop_reason", ...)logic in both non-streaming (_get_response_with_function_invocation) and streaming (_stream_response_with_function_invocation) loops.budget_state.setdefault("start_time", ...)inget_responseproperly preserves start time across approval resumes for cumulative timing.ResponseStreamfinalizer wrapper inget_responseensuring streaming updates remain clean and untouched.Related Issue
Fixes#7587
Contribution Checklist